@croptop/core-v6 0.0.23 → 0.0.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +1 -1
- package/ARCHITECTURE.md +1 -1
- package/AUDIT_INSTRUCTIONS.md +12 -10
- package/CHANGE_LOG.md +4 -0
- package/README.md +2 -2
- package/RISKS.md +4 -3
- package/SKILLS.md +1 -1
- package/USER_JOURNEYS.md +32 -15
- package/package.json +1 -1
- package/src/CTPublisher.sol +12 -3
package/ADMINISTRATION.md
CHANGED
|
@@ -142,6 +142,6 @@ What admins CANNOT do:
|
|
|
142
142
|
|
|
143
143
|
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.
|
|
144
144
|
|
|
145
|
-
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.
|
|
145
|
+
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 (or fallback recipients) within the same transaction. The fee terminal payment is wrapped in try-catch with fallback to `feeBeneficiary` then `msg.sender`, so ETH is never stranded by a fee terminal failure.
|
|
146
146
|
|
|
147
147
|
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.
|
package/ARCHITECTURE.md
CHANGED
|
@@ -65,7 +65,7 @@ Publisher → CTPublisher.mintFrom(hook, posts[], nftBeneficiary, feeBeneficiary
|
|
|
65
65
|
→ adjustTiers on the 721 hook to add new tiers
|
|
66
66
|
→ Pay the project terminal: payValue = msg.value - fee
|
|
67
67
|
Metadata encodes tier IDs to mint, so the 721 hook mints one NFT per post
|
|
68
|
-
→ Pay fee to FEE_PROJECT_ID terminal
|
|
68
|
+
→ Pay pre-computed fee to FEE_PROJECT_ID terminal (try-catch; falls back to feeBeneficiary, then msg.sender)
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
### Allowed Post Rules
|
package/AUDIT_INSTRUCTIONS.md
CHANGED
|
@@ -71,7 +71,7 @@ Quick reference for where the money is:
|
|
|
71
71
|
| Fee-free cashout | `CTDeployer.beforeCashOutRecordedWith()` | Full treasury value | Only legitimate suckers (via registry) get 0% tax |
|
|
72
72
|
| Unauthorized minting | `CTDeployer.hasMintPermissionFor()` | Arbitrary token minting | Only registered suckers get mint permission |
|
|
73
73
|
| Tier spam | `CTPublisher._setupPosts()` | Gas griefing, hook degradation | Allowlist + price/supply floors gate tier creation |
|
|
74
|
-
| Fee routing failure | `CTPublisher.mintFrom()`
|
|
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` |
|
|
75
75
|
|
|
76
76
|
---
|
|
77
77
|
|
|
@@ -126,9 +126,11 @@ Poster calls mintFrom(hook, posts[], nftBeneficiary, feeBeneficiary, additionalP
|
|
|
126
126
|
8. Look up project's primary ETH terminal via DIRECTORY [line 393-394]
|
|
127
127
|
terminal.pay{value: payValue}(...) [line 398-406]
|
|
128
128
|
|
|
129
|
-
9.
|
|
130
|
-
|
|
131
|
-
|
|
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]
|
|
132
134
|
```
|
|
133
135
|
|
|
134
136
|
### Fee Calculation
|
|
@@ -137,7 +139,7 @@ Poster calls mintFrom(hook, posts[], nftBeneficiary, feeBeneficiary, additionalP
|
|
|
137
139
|
- Fee = `totalPrice / 20` (integer division, truncates)
|
|
138
140
|
- Maximum rounding loss: 19 wei per transaction
|
|
139
141
|
- Fee is deducted from `msg.value` before the project payment
|
|
140
|
-
-
|
|
142
|
+
- Pre-computed fee (`msg.value - payValue`) goes to fee terminal via try-catch, with fallback to `feeBeneficiary` then `msg.sender`
|
|
141
143
|
- Fee is skipped entirely when `projectId == FEE_PROJECT_ID`
|
|
142
144
|
|
|
143
145
|
---
|
|
@@ -338,8 +340,8 @@ Check that no realistic usage pattern could cause a revert due to gas limits. Th
|
|
|
338
340
|
2. **Fee deduction and routing** (CTPublisher.sol lines 336-428). Verify:
|
|
339
341
|
- `payValue = msg.value - (totalPrice / FEE_DIVISOR)` cannot underflow
|
|
340
342
|
- The check `totalPrice > payValue` correctly prevents underpayment
|
|
341
|
-
- `
|
|
342
|
-
- The fee terminal payment cannot
|
|
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
|
|
343
345
|
|
|
344
346
|
3. **Data hook proxy forwarding** (CTDeployer.sol lines 132-169). Verify:
|
|
345
347
|
- `dataHookOf[projectId]` is always set before any pay/cashout can occur for that project
|
|
@@ -375,7 +377,7 @@ Check that no realistic usage pattern could cause a revert due to gas limits. Th
|
|
|
375
377
|
|
|
376
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.
|
|
377
379
|
|
|
378
|
-
12. **Force-sent ETH
|
|
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`.
|
|
379
381
|
|
|
380
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.
|
|
381
383
|
|
|
@@ -391,7 +393,7 @@ These properties should hold across all operations. They are suitable targets fo
|
|
|
391
393
|
|
|
392
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).
|
|
393
395
|
|
|
394
|
-
3. **ETH conservation:** For every `mintFrom()` call, `msg.value == payValue + feeAmount + dust`, where `dust <= 19 wei`.
|
|
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.
|
|
395
397
|
|
|
396
398
|
### Posting Invariants
|
|
397
399
|
|
|
@@ -447,7 +449,7 @@ ETHEREUM_RPC_URL=<your-rpc> forge test --match-contract ForkTest --fork-url $ETH
|
|
|
447
449
|
|
|
448
450
|
### Coverage Gaps (no existing tests)
|
|
449
451
|
|
|
450
|
-
- Force-sent ETH handling via selfdestruct
|
|
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)
|
|
451
453
|
- `deployProjectFor` front-running race condition
|
|
452
454
|
- Multiple hooks sharing the same CTPublisher instance
|
|
453
455
|
- Cross-category posting in a single batch (different categories, different allowlists)
|
package/CHANGE_LOG.md
CHANGED
|
@@ -242,6 +242,10 @@ No field changes. Import path updated from `@bananapus/suckers-v5` to `@bananapu
|
|
|
242
242
|
### `CTPublisher.mintFrom` — Named Arguments
|
|
243
243
|
- **v6:** Uses named arguments for `DIRECTORY.primaryTerminalOf(...)`, `hook.adjustTiers(...)`, `JBMetadataResolver.getId(...)`, and `_isAllowed(...)`.
|
|
244
244
|
|
|
245
|
+
### `CTPublisher.mintFrom` — Fee Payment Resilience (Audit Remediation)
|
|
246
|
+
- **Previous:** Fee terminal payment was a bare `feeTerminal.pay{value}()` call. If the fee terminal reverted, the entire `mintFrom()` transaction reverted, blocking all mints.
|
|
247
|
+
- **Current:** Fee amount is pre-computed as `msg.value - payValue` (no longer relies on `address(this).balance`). The fee terminal payment is wrapped in try-catch. On failure, the fee is sent to `feeBeneficiary` via low-level call. If that also fails, the fee is sent to `msg.sender`. A broken fee terminal never blocks mints.
|
|
248
|
+
|
|
245
249
|
### NatDoc / Comments
|
|
246
250
|
- **v6:** Adds extensive NatDoc comments to all interface functions, events, and struct fields. Adds `forge-lint` disable comments for mixed-case variables. Adds explanatory comments for design decisions (e.g., fee rounding behavior, force-sent ETH handling, category irrevocability, linear scan scaling).
|
|
247
251
|
|
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ Every `mintFrom` call collects a 5% fee on the total tier price. The fee is calc
|
|
|
56
56
|
|
|
57
57
|
- If the project being posted to **is** the fee project, no fee is collected (avoids circular payments).
|
|
58
58
|
- Integer division truncates, so the fee loses up to 19 wei of dust per mint.
|
|
59
|
-
-
|
|
59
|
+
- The fee amount is pre-computed as `msg.value - payValue` (not derived from `address(this).balance`), so force-sent ETH does not affect fee routing. The fee terminal payment is wrapped in try-catch with a fallback to `feeBeneficiary` then `msg.sender`.
|
|
60
60
|
|
|
61
61
|
### One-Click Deployment
|
|
62
62
|
|
|
@@ -173,4 +173,4 @@ script/
|
|
|
173
173
|
- **Fee skipping:** When `projectId == FEE_PROJECT_ID`, no fee is collected. This is intentional but means the fee project itself never pays Croptop fees.
|
|
174
174
|
- **Allowlist scaling:** `_isAllowed()` uses linear scan over the address allowlist. Large allowlists (100+ addresses) increase gas costs proportionally.
|
|
175
175
|
- **Tier reuse via IPFS URI:** If the same encoded IPFS URI has already been minted, the existing tier is reused rather than creating a new one. This prevents duplicate content but means a poster cannot create a second tier with the same content.
|
|
176
|
-
- **
|
|
176
|
+
- **Fee terminal failure:** The fee terminal payment is wrapped in try-catch. If the fee terminal reverts, the fee is sent to `feeBeneficiary` via low-level call, then to `msg.sender` if that also fails. A broken fee terminal never blocks mints, but the fee project loses revenue during the outage.
|
package/RISKS.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- **Fee evasion via duplicate posts across hooks.** `tierIdForEncodedIPFSUriOf` is keyed per hook. The same `encodedIPFSUri` can be posted to different hooks without duplicate detection, potentially creating fee-arbitrage opportunities.
|
|
15
15
|
- **Fee calculation rounding.** Fee is `totalPrice / FEE_DIVISOR` (FEE_DIVISOR=20, so 5% fee). Integer division truncates, losing up to 19 wei per post. Negligible individually but could compound across many micro-priced posts. Explicit validation: reverts `CTPublisher_InsufficientEthSent` if `msg.value < fee` (before subtraction) or if `msg.value - fee < totalPrice` (after subtraction).
|
|
16
16
|
- **Pre-computed fee routing.** `CTPublisher.mintFrom` computes the fee as `msg.value - payValue` before the external payment call, so the fee amount is determined from `msg.value` alone. Force-sent ETH (via selfdestruct) does not affect fee calculation.
|
|
17
|
+
- **Try-catch fee payment.** The fee terminal payment is wrapped in try-catch. If the fee terminal reverts, the fee is sent to `feeBeneficiary` via low-level call. If that also fails, the fee is sent to `msg.sender`. This means a broken fee terminal does not block mints, but the fee project may lose fee revenue during the outage.
|
|
17
18
|
- **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.
|
|
18
19
|
|
|
19
20
|
## 3. Access Control
|
|
@@ -33,8 +34,8 @@
|
|
|
33
34
|
|
|
34
35
|
## 5. Reentrancy Surface
|
|
35
36
|
|
|
36
|
-
- **`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) `
|
|
37
|
-
- **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`.
|
|
37
|
+
- **`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.
|
|
38
|
+
- **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`.
|
|
38
39
|
- **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.
|
|
39
40
|
|
|
40
41
|
## 6. Integration Risks
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
- **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.
|
|
44
45
|
- **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.
|
|
45
46
|
- **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.
|
|
46
|
-
- **Fee payment destination.** Fees are routed to `FEE_PROJECT_ID` via its primary terminal. If the fee project changes its terminal or token acceptance, fee payments
|
|
47
|
+
- **Fee payment destination.** Fees are routed to `FEE_PROJECT_ID` via its primary terminal. If the fee project changes its terminal or token acceptance, fee payments will fail. However, the fee terminal payment is wrapped in try-catch: on failure, the fee is sent to `feeBeneficiary` via low-level call, then to `msg.sender` if that also fails. Minting is never blocked by a broken fee terminal, but the fee project loses revenue during the outage.
|
|
47
48
|
|
|
48
49
|
## 7. Accepted Behaviors
|
|
49
50
|
|
package/SKILLS.md
CHANGED
|
@@ -127,7 +127,7 @@ Permissioned NFT publishing system that lets anyone post content as 721 tiers to
|
|
|
127
127
|
1. **Bit-packed allowances.** Allowances are packed into a single `uint256`: price in bits 0-103, min supply in 104-135, max supply in 136-167, max split percent in 168-199. Reading with wrong bit widths silently returns wrong values.
|
|
128
128
|
2. **Fee is 1/20, not a percentage.** `FEE_DIVISOR = 20` means fee = `totalPrice / 20` = 5%. Integer division truncates (rounding down favors payer).
|
|
129
129
|
3. **Fee skipped for fee project.** When `projectId == FEE_PROJECT_ID`, no fee is deducted. This prevents self-referential fee loops.
|
|
130
|
-
4. **Fee payment
|
|
130
|
+
4. **Fee payment is pre-computed and try-catch wrapped.** After the main payment, `mintFrom` computes the fee as `msg.value - payValue` and sends it to the fee terminal via try-catch. If the fee terminal reverts, the fee falls back to `feeBeneficiary.call{value}`, then `msg.sender.call{value}`. A broken fee terminal never blocks mints.
|
|
131
131
|
5. **Tier reuse by IPFS URI.** If an encoded IPFS URI was already minted on the hook, the existing tier ID is reused instead of creating a new tier. The poster still gets a mint of the existing tier. The fee is calculated from the actual tier price stored on-chain (not from `post.price`), preventing fee evasion (H-19 fix).
|
|
132
132
|
6. **Stale tier mapping cleanup.** If a tier was removed externally via `adjustTiers()`, the `tierIdForEncodedIPFSUriOf` mapping is automatically cleared when the same IPFS URI is posted again, allowing a new tier to be created (L-52 fix).
|
|
133
133
|
7. **Array resizing via assembly.** `_setupPosts` resizes `tiersToAdd` via inline assembly when some posts reuse existing tiers. The `tierIdsToMint` array is NOT resized and may contain zeros for pre-existing tiers.
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -168,11 +168,19 @@ Build JBMetadataResolver-compatible metadata with tier IDs and referral ID.
|
|
|
168
168
|
projectTerminal.pay{value: payValue}(projectId, NATIVE_TOKEN, payValue, nftBeneficiary, 0, "Minted from Croptop", mintMetadata)
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
**Phase 6: Fee payment** (lines
|
|
171
|
+
**Phase 6: Fee payment** (lines 409-438)
|
|
172
172
|
|
|
173
173
|
```
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
payValue = msg.value - payValue // reuse payValue for pre-computed fee amount
|
|
175
|
+
|
|
176
|
+
if (payValue != 0) {
|
|
177
|
+
feeTerminal = DIRECTORY.primaryTerminalOf(FEE_PROJECT_ID, NATIVE_TOKEN)
|
|
178
|
+
try feeTerminal.pay{value: payValue}(FEE_PROJECT_ID, ...) {}
|
|
179
|
+
catch {
|
|
180
|
+
// Fallback: send fee to feeBeneficiary, then msg.sender
|
|
181
|
+
feeBeneficiary.call{value: payValue}("")
|
|
182
|
+
// If that fails too, msg.sender.call{value: payValue}("")
|
|
183
|
+
}
|
|
176
184
|
}
|
|
177
185
|
```
|
|
178
186
|
|
|
@@ -205,9 +213,9 @@ if (address(this).balance != 0) {
|
|
|
205
213
|
- **Empty posts array:** `_setupPosts` returns with `totalPrice = 0`, `tiersToAdd` and `tierIdsToMint` both empty. `adjustTiers` is called with an empty array (no-op). The project terminal receives `msg.value` (no fee deducted since `totalPrice / 20 = 0`). The fee terminal receives nothing.
|
|
206
214
|
- **All posts reuse existing tiers:** No new tiers are created. `tiersToAdd` is resized to length 0 via assembly. `adjustTiers` is a no-op. Fees are calculated from on-chain tier prices.
|
|
207
215
|
- **Mixed new and existing tiers:** `tiersToAdd` is resized via assembly to contain only new tiers. `tierIdsToMint` contains a mix of new and existing IDs.
|
|
208
|
-
- **`msg.value` exceeds required amount:** Excess ETH
|
|
209
|
-
- **`msg.value` is exactly right:**
|
|
210
|
-
- **`projectId == FEE_PROJECT_ID`:** No fee is deducted. Full `msg.value` goes to the project terminal.
|
|
216
|
+
- **`msg.value` exceeds required amount:** Excess ETH goes to the fee project via the pre-computed fee (`msg.value - payValue`). The poster overpays the fee project.
|
|
217
|
+
- **`msg.value` is exactly right:** The pre-computed fee amount equals the expected fee. Fee project receives the correct amount.
|
|
218
|
+
- **`projectId == FEE_PROJECT_ID`:** No fee is deducted. Full `msg.value` goes to the project terminal. The pre-computed fee is 0 (no fee payment occurs).
|
|
211
219
|
- **Tier price is 0:** Posting criteria allow `minimumPrice = 0`. A post with `price = 0` creates a free tier. Fee on a free tier is 0. The poster sends 0 ETH (or only ETH for other posts in the batch).
|
|
212
220
|
- **`nftBeneficiary = address(0)`:** The terminal payment may succeed (depending on terminal implementation), but the NFTs would be minted to `address(0)`, effectively burning them.
|
|
213
221
|
- **`hook.adjustTiers()` reverts:** The entire transaction reverts. No state changes are committed. The poster's ETH is returned.
|
|
@@ -329,20 +337,29 @@ When `projectId == FEE_PROJECT_ID`, the fee calculation is skipped entirely. The
|
|
|
329
337
|
|
|
330
338
|
### Fee Routing
|
|
331
339
|
|
|
332
|
-
After the project payment completes
|
|
340
|
+
After the project payment completes, the fee amount is pre-computed as `msg.value - payValue`:
|
|
333
341
|
|
|
334
342
|
```solidity
|
|
335
|
-
|
|
343
|
+
payValue = msg.value - payValue; // reuse payValue for the pre-computed fee amount
|
|
344
|
+
|
|
345
|
+
if (payValue != 0) {
|
|
336
346
|
IJBTerminal feeTerminal = DIRECTORY.primaryTerminalOf(FEE_PROJECT_ID, NATIVE_TOKEN);
|
|
337
|
-
feeTerminal.pay{value:
|
|
347
|
+
try feeTerminal.pay{value: payValue}({
|
|
338
348
|
projectId: FEE_PROJECT_ID,
|
|
339
|
-
amount:
|
|
349
|
+
amount: payValue,
|
|
340
350
|
token: NATIVE_TOKEN,
|
|
341
351
|
beneficiary: feeBeneficiary,
|
|
342
352
|
minReturnedTokens: 0,
|
|
343
353
|
memo: "",
|
|
344
354
|
metadata: feeMetadata
|
|
345
|
-
})
|
|
355
|
+
}) {}
|
|
356
|
+
catch {
|
|
357
|
+
// Fallback: send fee to feeBeneficiary, then msg.sender if that fails too.
|
|
358
|
+
(bool success,) = feeBeneficiary.call{value: payValue}("");
|
|
359
|
+
if (!success) {
|
|
360
|
+
(success,) = msg.sender.call{value: payValue}("");
|
|
361
|
+
}
|
|
362
|
+
}
|
|
346
363
|
}
|
|
347
364
|
```
|
|
348
365
|
|
|
@@ -368,10 +385,10 @@ The `feeBeneficiary` parameter in `mintFrom()` determines who receives the fee p
|
|
|
368
385
|
|
|
369
386
|
### Edge Cases
|
|
370
387
|
|
|
371
|
-
- **Fee project has no primary terminal:** `DIRECTORY.primaryTerminalOf()` returns `address(0)`. The `pay()` call to address(0) reverts. The
|
|
372
|
-
- **Fee terminal reverts:**
|
|
373
|
-
-
|
|
374
|
-
- **Force-sent ETH (via `selfdestruct`):**
|
|
388
|
+
- **Fee project has no primary terminal:** `DIRECTORY.primaryTerminalOf()` returns `address(0)`. The `pay()` call to address(0) reverts inside the try-catch. The fee falls back to `feeBeneficiary.call{value}`, then `msg.sender.call{value}`. The main project payment is not affected.
|
|
389
|
+
- **Fee terminal reverts:** The fee terminal payment is wrapped in try-catch. If the fee terminal reverts, the fee is sent to `feeBeneficiary` via low-level call. If that also fails, the fee is sent to `msg.sender`. The main project payment is never rolled back due to a fee terminal failure. The fee project loses revenue during the outage.
|
|
390
|
+
- **Pre-computed fee is 0:** This happens when `fee == 0` (e.g., `totalPrice < FEE_DIVISOR` or `projectId == FEE_PROJECT_ID`). The fee payment is skipped entirely.
|
|
391
|
+
- **Force-sent ETH (via `selfdestruct`):** The fee amount is now pre-computed as `msg.value - payValue` rather than using `address(this).balance`. Force-sent ETH does not affect fee calculation or routing. CTPublisher has no `receive()` or `fallback()`, so normal sends revert. Only `selfdestruct` (deprecated post-Dencun) can force-send ETH, and it remains in the contract.
|
|
375
392
|
|
|
376
393
|
---
|
|
377
394
|
|
package/package.json
CHANGED
package/src/CTPublisher.sol
CHANGED
|
@@ -416,9 +416,9 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
416
416
|
IJBTerminal feeTerminal =
|
|
417
417
|
DIRECTORY.primaryTerminalOf({projectId: FEE_PROJECT_ID, token: JBConstants.NATIVE_TOKEN});
|
|
418
418
|
|
|
419
|
-
// Make the fee payment.
|
|
419
|
+
// Make the fee payment. Wrapped in try-catch so a reverting fee terminal doesn't block mints.
|
|
420
420
|
// slither-disable-next-line unused-return
|
|
421
|
-
feeTerminal.pay{value: payValue}({
|
|
421
|
+
try feeTerminal.pay{value: payValue}({
|
|
422
422
|
projectId: FEE_PROJECT_ID,
|
|
423
423
|
amount: payValue,
|
|
424
424
|
token: JBConstants.NATIVE_TOKEN,
|
|
@@ -426,7 +426,16 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
426
426
|
minReturnedTokens: 0,
|
|
427
427
|
memo: "",
|
|
428
428
|
metadata: feeMetadata
|
|
429
|
-
})
|
|
429
|
+
}) {}
|
|
430
|
+
catch {
|
|
431
|
+
// If the fee payment fails, send the fee to the beneficiary instead.
|
|
432
|
+
(bool success,) = feeBeneficiary.call{value: payValue}("");
|
|
433
|
+
if (!success) {
|
|
434
|
+
// If that also fails, send to the msg.sender.
|
|
435
|
+
// slither-disable-next-line low-level-calls
|
|
436
|
+
(success,) = msg.sender.call{value: payValue}("");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
430
439
|
}
|
|
431
440
|
}
|
|
432
441
|
|