@croptop/core-v6 0.0.27 → 0.0.29

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,263 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+
7
+ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
8
+ import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
9
+ import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
10
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
11
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
12
+ import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
13
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
14
+ import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
15
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
16
+
17
+ import {CTPublisher} from "../../src/CTPublisher.sol";
18
+ import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
19
+ import {CTPost} from "../../src/structs/CTPost.sol";
20
+
21
+ contract BlackholeMockPermissions is IJBPermissions {
22
+ // forge-lint: disable-next-line(mixed-case-function)
23
+ function WILDCARD_PROJECT_ID() external pure returns (uint256) {
24
+ return 0;
25
+ }
26
+
27
+ function permissionsOf(address, address, uint256) external pure returns (uint256) {
28
+ return 0;
29
+ }
30
+
31
+ function hasPermission(address, address, uint256, uint256, bool, bool) external pure returns (bool) {
32
+ return true;
33
+ }
34
+
35
+ function hasPermissions(address, address, uint256, uint256[] calldata, bool, bool) external pure returns (bool) {
36
+ return true;
37
+ }
38
+
39
+ function setPermissionsFor(address, JBPermissionsData calldata) external {}
40
+ }
41
+
42
+ contract BlackholeMockStore {
43
+ function maxTierIdOf(address) external pure returns (uint256) {
44
+ return 0;
45
+ }
46
+
47
+ function isTierRemoved(address, uint256) external pure returns (bool) {
48
+ return false;
49
+ }
50
+
51
+ function tierOf(address, uint256, bool) external pure returns (JB721Tier memory tier) {
52
+ return tier;
53
+ }
54
+ }
55
+
56
+ contract BlackholeMockHook {
57
+ uint256 public immutable PROJECT_ID;
58
+ IJB721TiersHookStore public immutable STORE;
59
+ address public immutable OWNER;
60
+
61
+ constructor(uint256 projectId, IJB721TiersHookStore store_, address owner_) {
62
+ PROJECT_ID = projectId;
63
+ STORE = store_;
64
+ OWNER = owner_;
65
+ }
66
+
67
+ function adjustTiers(JB721TierConfig[] calldata, uint256[] calldata) external {}
68
+
69
+ function METADATA_ID_TARGET() external view returns (address) {
70
+ return address(this);
71
+ }
72
+
73
+ function owner() external view returns (address) {
74
+ return OWNER;
75
+ }
76
+ }
77
+
78
+ contract AcceptingProjectTerminal {
79
+ uint256 public totalReceived;
80
+
81
+ function pay(
82
+ uint256,
83
+ address,
84
+ uint256,
85
+ address,
86
+ uint256,
87
+ string calldata,
88
+ bytes calldata
89
+ )
90
+ external
91
+ payable
92
+ returns (uint256)
93
+ {
94
+ totalReceived += msg.value;
95
+ return 0;
96
+ }
97
+ }
98
+
99
+ contract RevertingFeeTerminal {
100
+ error FeeTerminalDown();
101
+
102
+ function pay(
103
+ uint256,
104
+ address,
105
+ uint256,
106
+ address,
107
+ uint256,
108
+ string calldata,
109
+ bytes calldata
110
+ )
111
+ external
112
+ payable
113
+ returns (uint256)
114
+ {
115
+ revert FeeTerminalDown();
116
+ }
117
+ }
118
+
119
+ contract BlackholeDirectory {
120
+ address public projectTerminal;
121
+ address public feeTerminal;
122
+
123
+ function setTerminals(address projectTerminal_, address feeTerminal_) external {
124
+ projectTerminal = projectTerminal_;
125
+ feeTerminal = feeTerminal_;
126
+ }
127
+
128
+ function primaryTerminalOf(uint256 projectId, address) external view returns (IJBTerminal) {
129
+ return IJBTerminal(projectId == 1 ? feeTerminal : projectTerminal);
130
+ }
131
+ }
132
+
133
+ contract RejectingFeeBeneficiary {
134
+ receive() external payable {
135
+ revert("no fee");
136
+ }
137
+ }
138
+
139
+ contract RejectingMintCaller {
140
+ function execute(
141
+ CTPublisher publisher,
142
+ IJB721TiersHook hook,
143
+ CTPost[] memory posts,
144
+ address nftBeneficiary,
145
+ address feeBeneficiary
146
+ )
147
+ external
148
+ payable
149
+ {
150
+ publisher.mintFrom{value: msg.value}(hook, posts, nftBeneficiary, feeBeneficiary, bytes(""), bytes(""));
151
+ }
152
+
153
+ receive() external payable {
154
+ revert("no refund");
155
+ }
156
+ }
157
+
158
+ contract AcceptingMintCaller {
159
+ function execute(
160
+ CTPublisher publisher,
161
+ IJB721TiersHook hook,
162
+ CTPost[] memory posts,
163
+ address nftBeneficiary,
164
+ address feeBeneficiary
165
+ )
166
+ external
167
+ payable
168
+ {
169
+ publisher.mintFrom{value: msg.value}(hook, posts, nftBeneficiary, feeBeneficiary, bytes(""), bytes(""));
170
+ }
171
+
172
+ receive() external payable {}
173
+ }
174
+
175
+ contract FeeFallbackBlackholeTest is Test {
176
+ BlackholeMockPermissions permissions;
177
+ BlackholeDirectory directory;
178
+ BlackholeMockStore store;
179
+ BlackholeMockHook hook;
180
+ AcceptingProjectTerminal projectTerminal;
181
+ RevertingFeeTerminal feeTerminal;
182
+ RejectingFeeBeneficiary feeBeneficiary;
183
+ RejectingMintCaller caller;
184
+ AcceptingMintCaller acceptingCaller;
185
+ CTPublisher publisher;
186
+
187
+ function setUp() public {
188
+ permissions = new BlackholeMockPermissions();
189
+ directory = new BlackholeDirectory();
190
+ store = new BlackholeMockStore();
191
+ hook = new BlackholeMockHook(2, IJB721TiersHookStore(address(store)), address(this));
192
+ projectTerminal = new AcceptingProjectTerminal();
193
+ feeTerminal = new RevertingFeeTerminal();
194
+ feeBeneficiary = new RejectingFeeBeneficiary();
195
+ caller = new RejectingMintCaller();
196
+ acceptingCaller = new AcceptingMintCaller();
197
+ publisher = new CTPublisher(IJBDirectory(address(directory)), permissions, 1, address(0));
198
+
199
+ directory.setTerminals(address(projectTerminal), address(feeTerminal));
200
+
201
+ CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
202
+ allowedPosts[0] = CTAllowedPost({
203
+ hook: address(hook),
204
+ category: 1,
205
+ minimumPrice: 1,
206
+ minimumTotalSupply: 1,
207
+ maximumTotalSupply: type(uint32).max,
208
+ maximumSplitPercent: 0,
209
+ allowedAddresses: new address[](0)
210
+ });
211
+ publisher.configurePostingCriteriaFor(allowedPosts);
212
+
213
+ vm.deal(address(caller), 105);
214
+ vm.deal(address(acceptingCaller), 105);
215
+ }
216
+
217
+ function test_feePaymentFailure_refundsMsgSenderAndPreservesMint() public {
218
+ CTPost[] memory posts = new CTPost[](1);
219
+ posts[0] = CTPost({
220
+ encodedIPFSUri: keccak256("post"),
221
+ totalSupply: 1,
222
+ price: 100,
223
+ category: 1,
224
+ splitPercent: 0,
225
+ splits: new JBSplit[](0)
226
+ });
227
+
228
+ vm.prank(address(acceptingCaller));
229
+ acceptingCaller.execute{value: 105}(
230
+ publisher, IJB721TiersHook(address(hook)), posts, address(this), address(feeBeneficiary)
231
+ );
232
+
233
+ assertEq(projectTerminal.totalReceived(), 100, "main project payment should still succeed");
234
+ assertEq(address(feeTerminal).balance, 0, "fee terminal should receive nothing after reverting");
235
+ assertEq(address(feeBeneficiary).balance, 0, "fee beneficiary should receive nothing");
236
+ assertEq(address(acceptingCaller).balance, 5, "caller should receive the refunded fee");
237
+ assertEq(address(publisher).balance, 0, "publisher should not retain trapped fees");
238
+ }
239
+
240
+ function test_feePaymentFailure_revertsIfMsgSenderRejectsRefund() public {
241
+ CTPost[] memory posts = new CTPost[](1);
242
+ posts[0] = CTPost({
243
+ encodedIPFSUri: keccak256("post"),
244
+ totalSupply: 1,
245
+ price: 100,
246
+ category: 1,
247
+ splitPercent: 0,
248
+ splits: new JBSplit[](0)
249
+ });
250
+
251
+ vm.prank(address(caller));
252
+ vm.expectRevert(abi.encodeWithSelector(CTPublisher.CTPublisher_FeePaymentFailed.selector, 5));
253
+ caller.execute{value: 105}(
254
+ publisher, IJB721TiersHook(address(hook)), posts, address(this), address(feeBeneficiary)
255
+ );
256
+
257
+ assertEq(projectTerminal.totalReceived(), 0, "main project payment should roll back with the fee failure");
258
+ assertEq(address(feeTerminal).balance, 0, "fee terminal should receive nothing after reverting");
259
+ assertEq(address(feeBeneficiary).balance, 0, "fee beneficiary should receive nothing");
260
+ assertEq(address(caller).balance, 105, "caller should retain funds when the mint reverts");
261
+ assertEq(address(publisher).balance, 0, "publisher should not retain trapped fees");
262
+ }
263
+ }
@@ -11,6 +11,7 @@ import {IJB721Hook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Hook.sol";
11
11
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
12
12
  import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
13
13
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
14
+ import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
14
15
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
15
16
 
16
17
  import {CTPublisher} from "../../src/CTPublisher.sol";
@@ -110,11 +111,13 @@ contract H19_FeeEvasion is Test {
110
111
  encodedIPFSUri: TEST_URI,
111
112
  category: 5,
112
113
  discountPercent: 0,
113
- allowOwnerMint: false,
114
- transfersPausable: false,
115
- cantBeRemoved: false,
116
- cantIncreaseDiscountPercent: false,
117
- cantBuyWithCredits: false,
114
+ flags: JB721TierFlags({
115
+ allowOwnerMint: false,
116
+ transfersPausable: false,
117
+ cantBeRemoved: false,
118
+ cantIncreaseDiscountPercent: false,
119
+ cantBuyWithCredits: false
120
+ }),
118
121
  splitPercent: 0,
119
122
  resolvedUri: ""
120
123
  });
@@ -207,11 +210,13 @@ contract H19_FeeEvasion is Test {
207
210
  encodedIPFSUri: TEST_URI,
208
211
  category: 5,
209
212
  discountPercent: 0,
210
- allowOwnerMint: false,
211
- transfersPausable: false,
212
- cantBeRemoved: false,
213
- cantIncreaseDiscountPercent: false,
214
- cantBuyWithCredits: false,
213
+ flags: JB721TierFlags({
214
+ allowOwnerMint: false,
215
+ transfersPausable: false,
216
+ cantBeRemoved: false,
217
+ cantIncreaseDiscountPercent: false,
218
+ cantBuyWithCredits: false
219
+ }),
215
220
  splitPercent: 0,
216
221
  resolvedUri: ""
217
222
  });
@@ -11,6 +11,7 @@ import {IJB721Hook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Hook.sol";
11
11
  import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
12
12
  import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
13
13
  import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
14
+ import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.sol";
14
15
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
15
16
 
16
17
  import {CTPublisher} from "../../src/CTPublisher.sol";
@@ -170,11 +171,13 @@ contract L52_StaleTierIdMapping is Test {
170
171
  encodedIPFSUri: TEST_URI,
171
172
  category: 5,
172
173
  discountPercent: 0,
173
- allowOwnerMint: false,
174
- transfersPausable: false,
175
- cantBeRemoved: false,
176
- cantIncreaseDiscountPercent: false,
177
- cantBuyWithCredits: false,
174
+ flags: JB721TierFlags({
175
+ allowOwnerMint: false,
176
+ transfersPausable: false,
177
+ cantBeRemoved: false,
178
+ cantIncreaseDiscountPercent: false,
179
+ cantBuyWithCredits: false
180
+ }),
178
181
  splitPercent: 0,
179
182
  resolvedUri: ""
180
183
  });
package/CHANGE_LOG.md DELETED
@@ -1,273 +0,0 @@
1
- # croptop-core-v6 Changelog (v5 → v6)
2
-
3
- This document describes all changes between `croptop-core` (v5) and `croptop-core-v6` (v6).
4
-
5
- ## Summary
6
-
7
- - **Data hook proxy activated**: `CTDeployer` now sets itself as the data hook (`metadata.dataHook = address(this)`) instead of pointing directly to the 721 hook — enables sucker cashouts at 0% tax rate for cross-chain operations.
8
- - **Split support for posts**: `CTPost` gained `splitPercent` and `splits` fields, allowing poster-defined payment routing per NFT tier (bounded by `maximumSplitPercent`).
9
- - **Fee evasion fixes**: Existing tier mints now use on-chain price (not user-supplied), and duplicate posts within a batch are rejected.
10
- - **Stale tier recovery**: Externally-removed tiers are detected and re-created instead of silently failing.
11
- - **`projectId` cast widened**: `uint56` → `uint64` to match v6 `JBPermissionsData`.
12
-
13
- ---
14
-
15
- ## 1. Breaking Changes
16
-
17
- ### Solidity Version
18
- - Compiler version bumped from `0.8.23` to `0.8.28` across all implementation contracts (`CTDeployer`, `CTProjectOwner`, `CTPublisher`).
19
-
20
- ### Dependency Namespace Migration
21
- All imports updated from v5 to v6 namespaces:
22
- - `@bananapus/core-v5` → `@bananapus/core-v6`
23
- - `@bananapus/721-hook-v5` → `@bananapus/721-hook-v6`
24
- - `@bananapus/ownable-v5` → `@bananapus/ownable-v6`
25
- - `@bananapus/permission-ids-v5` → `@bananapus/permission-ids-v6`
26
- - `@bananapus/suckers-v5` → `@bananapus/suckers-v6`
27
-
28
- ### `ICTPublisher.allowanceFor` Return Signature Changed
29
- - **v5:** Returns 4 values — `(uint256 minimumPrice, uint256 minimumTotalSupply, uint256 maximumTotalSupply, address[] memory allowedAddresses)`
30
- - **v6:** Returns 5 values — `(uint256 minimumPrice, uint256 minimumTotalSupply, uint256 maximumTotalSupply, uint256 maximumSplitPercent, address[] memory allowedAddresses)`
31
- - The new `maximumSplitPercent` return value is inserted before `allowedAddresses`. Any consumer destructuring this return value will break.
32
-
33
- ### `ICTPublisher.mintFrom` Parameter Data Location Changed
34
- - **v5:** `CTPost[] memory posts`
35
- - **v6:** `CTPost[] calldata posts`
36
-
37
- ### `CTPost` Struct Has New Fields
38
- - **v5:** `{ bytes32 encodedIPFSUri, uint32 totalSupply, uint104 price, uint24 category }`
39
- - **v6:** `{ bytes32 encodedIPFSUri, uint32 totalSupply, uint104 price, uint24 category, uint32 splitPercent, JBSplit[] splits }`
40
- - Adds `splitPercent` (uint32) and `splits` (JBSplit[] from `@bananapus/core-v6`). This changes the ABI encoding of `CTPost` and all functions that accept it.
41
-
42
- ### `CTAllowedPost` Struct Has New Field
43
- - **v5:** `{ address hook, uint24 category, uint104 minimumPrice, uint32 minimumTotalSupply, uint32 maximumTotalSupply, address[] allowedAddresses }`
44
- - **v6:** `{ address hook, uint24 category, uint104 minimumPrice, uint32 minimumTotalSupply, uint32 maximumTotalSupply, uint32 maximumSplitPercent, address[] allowedAddresses }`
45
- - Adds `maximumSplitPercent` (uint32) before `allowedAddresses`. This changes the ABI encoding of `CTAllowedPost` and all functions that accept it.
46
-
47
- ### `CTDeployerAllowedPost` Struct Has New Field
48
- - **v5:** `{ uint24 category, uint104 minimumPrice, uint32 minimumTotalSupply, uint32 maximumTotalSupply, address[] allowedAddresses }`
49
- - **v6:** `{ uint24 category, uint104 minimumPrice, uint32 minimumTotalSupply, uint32 maximumTotalSupply, uint32 maximumSplitPercent, address[] allowedAddresses }`
50
- - Adds `maximumSplitPercent` (uint32) before `allowedAddresses`, mirroring `CTAllowedPost`.
51
-
52
- ### `CTProjectOwner.onERC721Received` — `projectId` Cast Width Changed
53
- - **v5:** `projectId: uint56(tokenId)`
54
- - **v6:** `projectId: uint64(tokenId)`
55
- - This aligns with the v6 `JBPermissionsData` struct which uses `uint64` for `projectId` (was `uint56` in v5).
56
-
57
- ### `CTDeployer.deployProjectFor` — Data Hook and Cash Out Behavior Changed
58
- - **v5:** Sets `metadata.dataHook = address(hook)` (the 721 hook itself is the data hook). Does NOT set `cashOutTaxRate` or `useDataHookForCashOut`.
59
- - **v6:** Sets `metadata.dataHook = address(this)` (the CTDeployer itself is the data hook). Sets `metadata.cashOutTaxRate = JBConstants.MAX_CASH_OUT_TAX_RATE` and `metadata.useDataHookForCashOut = true`.
60
- - The CTDeployer now acts as a data hook proxy, forwarding pay/cashout calls to the stored `dataHookOf[projectId]`, while intercepting sucker cash outs to grant 0% tax rate. This is a fundamental architectural change.
61
-
62
- > **Why this change**: In v5, the CTDeployer already had the proxy methods (`beforePayRecordedWith`, `beforeCashOutRecordedWith`, `hasMintPermissionFor`) and the `dataHookOf` mapping, but `deployProjectFor` pointed `metadata.dataHook` directly at the 721 hook, bypassing the proxy entirely. v6 activates the proxy so the deployer can intercept sucker cashouts (verified via `SUCKER_REGISTRY.isSuckerOf`) and return a 0% tax rate for cross-chain operations. Without this, cross-chain token bridging via suckers would incur the full `MAX_CASH_OUT_TAX_RATE`, making omnichain projects economically unviable.
63
-
64
- ### `JB721InitTiersConfig` — `prices` Field Removed
65
- - **v5:** `JB721InitTiersConfig({ tiers, currency, decimals, prices: controller.PRICES() })`
66
- - **v6:** `JB721InitTiersConfig({ tiers, currency, decimals })` — the `prices` field no longer exists in the v6 721 hook config struct.
67
-
68
- ### `JB721TiersHookFlags` — New `issueTokensForSplits` Flag
69
- - **v5:** `JB721TiersHookFlags({ noNewTiersWithReserves, noNewTiersWithVotes, noNewTiersWithOwnerMinting, preventOverspending })`
70
- - **v6:** Adds `issueTokensForSplits: false` as a fifth flag.
71
-
72
- ### `ICTDeployer.deployProjectFor` — Parameter Renamed
73
- - **v5:** `projectConfigurations` parameter name
74
- - **v6:** `projectConfig` parameter name
75
-
76
- ---
77
-
78
- ## 2. New Features
79
-
80
- ### Split Percent Support for Posts
81
- Posts can now include a `splitPercent` and an array of `splits` (JBSplit[]) that route a percentage of the tier's price to specified recipients when the NFT is minted. This is enforced against a per-category `maximumSplitPercent` configured by the project owner.
82
-
83
- - `CTPost.splitPercent` — percent of tier price to route to splits (out of `JBConstants.SPLITS_TOTAL_PERCENT`).
84
- - `CTPost.splits` — the split recipients for the tier.
85
- - `CTAllowedPost.maximumSplitPercent` — the maximum split percent a poster can set (0 = splits not allowed).
86
- - `CTDeployerAllowedPost.maximumSplitPercent` — same as above, for deployer-configured posts.
87
- - `JB721TierConfig` in v6 now accepts `splitPercent` and `splits` fields, which are populated from the post.
88
-
89
- ### Duplicate Post Detection
90
- - v6 adds an explicit duplicate check within `_setupPosts`: if two posts in the same batch share the same `encodedIPFSUri`, the transaction reverts with `CTPublisher_DuplicatePost`. This prevents fee evasion by submitting duplicate URIs in a single `mintFrom` call.
91
-
92
- ### Stale Tier Cleanup
93
- - v6 adds logic to detect when a tier referenced by `tierIdForEncodedIPFSUriOf` has been removed externally (via `adjustTiers`). If `hook.STORE().isTierRemoved()` returns true, the stale mapping is deleted and a new tier is created for that URI.
94
-
95
- ### Fee Evasion Prevention for Existing Tiers
96
- - **v5:** When minting from an existing tier, `totalPrice` was accumulated using `post.price` (user-supplied).
97
- - **v6:** When minting from an existing tier, `totalPrice` is accumulated using the actual tier price fetched from `store.tierOf()`. This prevents a caller from passing `price=0` for an existing tier to evade fees.
98
-
99
- ### CTDeployer Data Hook Proxy Activated
100
- - The CTDeployer implemented the data hook proxy pattern in v5 as well -- it had `beforePayRecordedWith`, `beforeCashOutRecordedWith`, `hasMintPermissionFor`, and the `dataHookOf` mapping -- but `deployProjectFor` set `metadata.dataHook = address(hook)` (the 721 hook directly), so the proxy methods were never called. In v6, `deployProjectFor` sets `metadata.dataHook = address(this)`, `cashOutTaxRate = MAX_CASH_OUT_TAX_RATE`, and `useDataHookForCashOut = true`, activating the proxy. This routes all pay and cash out data hook calls through CTDeployer, which forwards them to the stored `dataHookOf[projectId]` while intercepting sucker cash outs (verified via `SUCKER_REGISTRY.isSuckerOf`) to return a 0% tax rate for cross-chain operations.
101
-
102
- ---
103
-
104
- ## 3. Event Changes
105
-
106
- Indexer note:
107
- - event names are stable, but embedded struct payloads changed ABI shape;
108
- - if your graph decodes `ConfigurePostingCriteria` or `Mint`, update the event-decoding schema for the new `maximumSplitPercent`, `splitPercent`, and `splits` fields.
109
-
110
- No event signatures were changed. Both versions emit the same two events:
111
- - `ConfigurePostingCriteria(address indexed hook, CTAllowedPost allowedPost, address caller)` — note that the `CTAllowedPost` struct gained a `maximumSplitPercent` field, which changes the ABI encoding of this event's data.
112
- - `Mint(uint256 indexed projectId, IJB721TiersHook indexed hook, address indexed nftBeneficiary, address feeBeneficiary, CTPost[] posts, uint256 postValue, uint256 txValue, address caller)` — note that the `CTPost` struct gained `splitPercent` and `splits` fields, which changes the ABI encoding of this event's data.
113
-
114
- ---
115
-
116
- ## 4. Error Changes
117
-
118
- ### New Errors
119
-
120
- | Error | Contract | Description |
121
- |-------|----------|-------------|
122
- | `CTPublisher_DuplicatePost(bytes32 encodedIPFSUri)` | `CTPublisher` | Reverts when two posts in the same `mintFrom` batch share the same encoded IPFS URI. |
123
- | `CTPublisher_SplitPercentExceedsMaximum(uint256 splitPercent, uint256 maximumSplitPercent)` | `CTPublisher` | Reverts when a post's split percent exceeds the category's configured maximum. |
124
-
125
- ### Unchanged Errors
126
- - `CTDeployer_NotOwnerOfProject(uint256 projectId, address hook, address caller)` — unchanged.
127
- - `CTPublisher_EmptyEncodedIPFSUri()` — unchanged.
128
- - `CTPublisher_InsufficientEthSent(uint256 expected, uint256 sent)` — signature unchanged; v6 adds an explicit fee validation check before the subtraction (`if (payValue < fee) revert`) so this error now fires with a descriptive message instead of a panic on underflow.
129
- - `CTPublisher_MaxTotalSupplyLessThanMin(uint256 min, uint256 max)` — unchanged.
130
- - `CTPublisher_NotInAllowList(address addr, address[] allowedAddresses)` — unchanged.
131
- - `CTPublisher_PriceTooSmall(uint256 price, uint256 minimumPrice)` — unchanged.
132
- - `CTPublisher_TotalSupplyTooBig(uint256 totalSupply, uint256 maximumTotalSupply)` — unchanged.
133
- - `CTPublisher_TotalSupplyTooSmall(uint256 totalSupply, uint256 minimumTotalSupply)` — unchanged.
134
- - `CTPublisher_UnauthorizedToPostInCategory()` — unchanged.
135
- - `CTPublisher_ZeroTotalSupply()` — unchanged.
136
-
137
- ---
138
-
139
- ## 5. Struct Changes
140
-
141
- ### `CTPost`
142
- | Field | v5 | v6 |
143
- |-------|----|----|
144
- | `encodedIPFSUri` | `bytes32` | `bytes32` |
145
- | `totalSupply` | `uint32` | `uint32` |
146
- | `price` | `uint104` | `uint104` |
147
- | `category` | `uint24` | `uint24` |
148
- | `splitPercent` | -- | `uint32` (new) |
149
- | `splits` | -- | `JBSplit[]` (new) |
150
-
151
- New import: `JBSplit` from `@bananapus/core-v6/src/structs/JBSplit.sol`.
152
-
153
- ### `CTAllowedPost`
154
- | Field | v5 | v6 |
155
- |-------|----|----|
156
- | `hook` | `address` | `address` |
157
- | `category` | `uint24` | `uint24` |
158
- | `minimumPrice` | `uint104` | `uint104` |
159
- | `minimumTotalSupply` | `uint32` | `uint32` |
160
- | `maximumTotalSupply` | `uint32` | `uint32` |
161
- | `maximumSplitPercent` | -- | `uint32` (new) |
162
- | `allowedAddresses` | `address[]` | `address[]` |
163
-
164
- ### `CTDeployerAllowedPost`
165
- | Field | v5 | v6 |
166
- |-------|----|----|
167
- | `category` | `uint24` | `uint24` |
168
- | `minimumPrice` | `uint104` | `uint104` |
169
- | `minimumTotalSupply` | `uint32` | `uint32` |
170
- | `maximumTotalSupply` | `uint32` | `uint32` |
171
- | `maximumSplitPercent` | -- | `uint32` (new) |
172
- | `allowedAddresses` | `address[]` | `address[]` |
173
-
174
- ### `CTProjectConfig`
175
- No field changes. Import path updated from `@bananapus/core-v5` to `@bananapus/core-v6` for `JBTerminalConfig`.
176
-
177
- ### `CTSuckerDeploymentConfig`
178
- No field changes. Import path updated from `@bananapus/suckers-v5` to `@bananapus/suckers-v6` for `JBSuckerDeployerConfig`.
179
-
180
- ---
181
-
182
- ## 6. Implementation Changes (Non-Interface)
183
-
184
- ### `CTPublisher._setupPosts`
185
-
186
- #### Store Reference Caching
187
- - **v5:** Calls `hook.STORE().maxTierIdOf(...)` inline, accessing the store through the hook each time.
188
- - **v6:** Caches `IJB721TiersHookStore store = hook.STORE()` once and reuses it. Also imports `IJB721TiersHookStore` explicitly.
189
-
190
- #### Duplicate Post Detection
191
- - **v6 only:** Adds an O(n^2) check at the start of each post iteration that scans all previous posts for matching `encodedIPFSUri`. Reverts with `CTPublisher_DuplicatePost` on match.
192
-
193
- #### Stale Tier Recovery
194
- - **v5:** If `tierIdForEncodedIPFSUriOf` returns a nonzero tier ID, it is used unconditionally.
195
- - **v6:** Checks `hook.STORE().isTierRemoved(address(hook), tierId)`. If removed, deletes the stale mapping and falls through to create a new tier.
196
-
197
- #### Fee-Accurate Price for Existing Tiers
198
- - **v5:** `totalPrice += post.price` for all posts (new and existing).
199
- - **v6:** For existing tiers, `totalPrice += store.tierOf(...).price` (uses actual on-chain price). For new tiers, `totalPrice += post.price`.
200
-
201
- #### Split Validation
202
- - **v6 only:** Checks `post.splitPercent > maximumSplitPercent` and reverts with `CTPublisher_SplitPercentExceedsMaximum` if exceeded.
203
-
204
- #### `JB721TierConfig` Construction
205
- - **v5:** 14 fields in `JB721TierConfig`.
206
- - **v6:** 16 fields — adds `splitPercent: post.splitPercent` and `splits: post.splits`.
207
-
208
- ### `CTPublisher.allowanceFor` — Packed Storage Layout Extended
209
- - **v5:** Packs 3 fields into `_packedAllowanceFor`: bits 0-103 (minimumPrice), 104-135 (minimumTotalSupply), 136-167 (maximumTotalSupply). Total: 168 bits.
210
- - **v6:** Packs 4 fields: bits 0-103, 104-135, 136-167 as before, plus bits 168-199 (maximumSplitPercent, 32 bits). Total: 200 bits.
211
-
212
- ### `CTPublisher.configurePostingCriteriaFor` — Packs `maximumSplitPercent`
213
- - **v6 only:** Adds `packed |= uint256(allowedPost.maximumSplitPercent) << 168;` when storing allowance data.
214
-
215
- ### `CTDeployer._configurePostingCriteriaFor` — Passes `maximumSplitPercent`
216
- - **v5:** `CTAllowedPost` construction has 6 fields.
217
- - **v6:** `CTAllowedPost` construction has 7 fields — adds `maximumSplitPercent: post.maximumSplitPercent`.
218
-
219
- ### `CTDeployer.deployProjectFor` — Ruleset Configuration Changes
220
- - **v5:** Sets `metadata.dataHook = address(hook)` and `metadata.useDataHookForPay = true`.
221
- - **v6:** Sets `metadata.cashOutTaxRate = JBConstants.MAX_CASH_OUT_TAX_RATE`, `metadata.dataHook = address(this)`, `metadata.useDataHookForPay = true`, and `metadata.useDataHookForCashOut = true`. Imports `JBConstants` for this.
222
-
223
- ### `CTDeployer.deployProjectFor` — Named Arguments in Function Calls
224
- - **v6:** Uses named arguments consistently (e.g., `PROJECTS.transferFrom({from: ..., to: ..., tokenId: ...})` instead of positional arguments).
225
-
226
- ### `CTDeployer` — Function Ordering
227
- - **v5:** `beforePayRecordedWith` appears before `beforeCashOutRecordedWith` in source.
228
- - **v6:** `beforeCashOutRecordedWith` appears before `beforePayRecordedWith`. Similarly, `claimCollectionOwnershipOf` appears before `deployProjectFor` in v6 (reversed from v5).
229
-
230
- ### `CTDeployer.beforeCashOutRecordedWith` — Named Arguments
231
- - **v5:** `SUCKER_REGISTRY.isSuckerOf(context.projectId, context.holder)`
232
- - **v6:** `SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})`
233
-
234
- ### `CTDeployer.hasMintPermissionFor` — Named Arguments
235
- - **v5:** `SUCKER_REGISTRY.isSuckerOf(projectId, addr)`
236
- - **v6:** `SUCKER_REGISTRY.isSuckerOf({projectId: projectId, addr: addr})`
237
-
238
- ### `CTPublisher.tiersFor` — Named Arguments
239
- - **v5:** `IJB721TiersHook(hook).STORE().tierOf(hook, tierId, false)`
240
- - **v6:** `IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false})`
241
-
242
- ### `CTPublisher.mintFrom` — Named Arguments
243
- - **v6:** Uses named arguments for `DIRECTORY.primaryTerminalOf(...)`, `hook.adjustTiers(...)`, `JBMetadataResolver.getId(...)`, and `_isAllowed(...)`.
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
-
249
- ### NatDoc / Comments
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).
251
-
252
- ---
253
-
254
- ## 7. Migration Table
255
-
256
- | v5 Identifier | v6 Identifier | Change |
257
- |---------------|---------------|--------|
258
- | `CTPost.{4 fields}` | `CTPost.{6 fields}` | Added `splitPercent`, `splits` |
259
- | `CTAllowedPost.{6 fields}` | `CTAllowedPost.{7 fields}` | Added `maximumSplitPercent` |
260
- | `CTDeployerAllowedPost.{5 fields}` | `CTDeployerAllowedPost.{6 fields}` | Added `maximumSplitPercent` |
261
- | `ICTPublisher.allowanceFor` (4 returns) | `ICTPublisher.allowanceFor` (5 returns) | Added `maximumSplitPercent` return |
262
- | `ICTPublisher.mintFrom(... CTPost[] memory ...)` | `ICTPublisher.mintFrom(... CTPost[] calldata ...)` | `memory` → `calldata` |
263
- | `CTProjectOwner`: `uint56(tokenId)` | `CTProjectOwner`: `uint64(tokenId)` | Cast width for projectId |
264
- | `CTDeployer`: `dataHook = address(hook)` | `CTDeployer`: `dataHook = address(this)` | CTDeployer is now the data hook |
265
- | `CTDeployer`: no cashout config | `CTDeployer`: `cashOutTaxRate = MAX`, `useDataHookForCashOut = true` | Enables sucker 0% tax cashout |
266
- | `JB721InitTiersConfig`: has `prices` | `JB721InitTiersConfig`: no `prices` | Field removed in v6 721 hook |
267
- | `JB721TiersHookFlags`: 4 flags | `JB721TiersHookFlags`: 5 flags | Added `issueTokensForSplits` |
268
- | -- | `CTPublisher_DuplicatePost` | New error |
269
- | -- | `CTPublisher_SplitPercentExceedsMaximum` | New error |
270
- | Solidity `0.8.23` | Solidity `0.8.28` | Compiler bump |
271
- | `@bananapus/*-v5` | `@bananapus/*-v6` | All dependency namespaces |
272
-
273
- > **Cross-repo impact**: The `CTPost.splitPercent` and `splits` fields feed directly into `nana-721-hook-v6`'s tier splits system. `nana-suckers-v6` suckers are detected via `SUCKER_REGISTRY.isSuckerOf` for the 0% tax cashout path. `nana-permission-ids-v6` `uint64` projectId width change drove the `CTProjectOwner` cast update.