@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.
- package/ADMINISTRATION.md +30 -3
- package/ARCHITECTURE.md +49 -157
- package/AUDIT_INSTRUCTIONS.md +69 -485
- package/CHANGELOG.md +57 -0
- package/README.md +54 -137
- package/RISKS.md +27 -3
- package/SKILLS.md +28 -187
- package/STYLE_GUIDE.md +56 -17
- package/USER_JOURNEYS.md +37 -708
- package/package.json +5 -6
- package/references/operations.md +25 -0
- package/references/runtime.md +27 -0
- package/script/Deploy.s.sol +4 -23
- package/src/CTDeployer.sol +5 -1
- package/src/CTPublisher.sol +16 -15
- package/test/CTPublisher.t.sol +10 -7
- package/test/audit/DeployerPermissionBypass.t.sol +213 -0
- package/test/audit/FeeFallbackBlackhole.t.sol +263 -0
- package/test/regression/FeeEvasion.t.sol +15 -10
- package/test/regression/StaleTierIdMapping.t.sol +8 -5
- package/CHANGE_LOG.md +0 -273
|
@@ -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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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.
|