@croptop/core-v6 0.0.38 → 0.0.40
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/README.md +2 -2
- package/foundry.toml +2 -1
- package/package.json +25 -13
- package/script/ConfigureFeeProject.s.sol +8 -5
- package/src/CTDeployer.sol +67 -58
- package/src/CTProjectOwner.sol +6 -4
- package/src/CTPublisher.sol +14 -4
- package/src/interfaces/ICTDeployer.sol +2 -2
- package/src/structs/CTProjectConfig.sol +7 -6
- package/ADMINISTRATION.md +0 -94
- package/ARCHITECTURE.md +0 -96
- package/AUDIT_INSTRUCTIONS.md +0 -88
- package/RISKS.md +0 -78
- package/SKILLS.md +0 -46
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -134
- package/foundry.lock +0 -11
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -507
- package/test/CTDeployer.t.sol +0 -616
- package/test/CTProjectOwner.t.sol +0 -185
- package/test/CTPublisher.t.sol +0 -869
- package/test/ClaimCollectionOwnership.t.sol +0 -315
- package/test/CroptopAttacks.t.sol +0 -437
- package/test/Fork.t.sol +0 -227
- package/test/TestAuditGaps.sol +0 -696
- package/test/Test_MetadataGeneration.t.sol +0 -79
- package/test/audit/CodexNemesisCroptopPublisherBoundary.t.sol +0 -329
- package/test/audit/CodexNemesisCurrencyPoCs.t.sol +0 -371
- package/test/audit/CodexNemesisFreshRound.t.sol +0 -395
- package/test/audit/CodexNemesisMetadataShadow.t.sol +0 -196
- package/test/audit/CodexNemesisPoCs.t.sol +0 -263
- package/test/audit/CodexNemesisPolicyReuse.t.sol +0 -168
- package/test/audit/CodexNemesisUriDrift.t.sol +0 -252
- package/test/audit/DeployerPermissionBypass.t.sol +0 -213
- package/test/audit/EmptyPostFeeBypass.t.sol +0 -53
- package/test/audit/FeeBeneficiaryReentrancy.t.sol +0 -247
- package/test/audit/FeeFallbackBlackhole.t.sol +0 -263
- package/test/audit/Pass12Fixes.t.sol +0 -388
- package/test/fork/PublishFork.t.sol +0 -440
- package/test/regression/DuplicateUriFeeEvasion.t.sol +0 -312
- package/test/regression/FeeEvasion.t.sol +0 -286
- package/test/regression/StaleTierIdMapping.t.sol +0 -228
|
@@ -1,263 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,388 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {Test} from "forge-std/Test.sol";
|
|
5
|
-
|
|
6
|
-
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
7
|
-
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
8
|
-
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
9
|
-
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
10
|
-
import {JB721TierFlags} from "@bananapus/721-hook-v6/src/structs/JB721TierFlags.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 {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
15
|
-
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
16
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
17
|
-
|
|
18
|
-
import {CTPublisher} from "../../src/CTPublisher.sol";
|
|
19
|
-
import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
|
|
20
|
-
import {CTPost} from "../../src/structs/CTPost.sol";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Minimal mock contracts (reusable across both tests)
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
contract P12MockTerminal {
|
|
27
|
-
function pay(
|
|
28
|
-
uint256,
|
|
29
|
-
address,
|
|
30
|
-
uint256,
|
|
31
|
-
address,
|
|
32
|
-
uint256,
|
|
33
|
-
string calldata,
|
|
34
|
-
bytes calldata
|
|
35
|
-
)
|
|
36
|
-
external
|
|
37
|
-
payable
|
|
38
|
-
returns (uint256)
|
|
39
|
-
{
|
|
40
|
-
return 0;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
contract P12MockDirectory {
|
|
45
|
-
// forge-lint: disable-next-line(screaming-snake-case-immutable)
|
|
46
|
-
IJBTerminal internal immutable _terminal;
|
|
47
|
-
|
|
48
|
-
constructor(IJBTerminal terminal_) {
|
|
49
|
-
_terminal = terminal_;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function primaryTerminalOf(uint256, address) external view returns (IJBTerminal) {
|
|
53
|
-
return _terminal;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
contract P12MockStore {
|
|
58
|
-
struct TierData {
|
|
59
|
-
bytes32 uri;
|
|
60
|
-
uint104 price;
|
|
61
|
-
uint24 category;
|
|
62
|
-
uint32 supply;
|
|
63
|
-
bool removed;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
uint256 internal _maxTierId;
|
|
67
|
-
mapping(uint256 tierId => TierData) internal _tiers;
|
|
68
|
-
|
|
69
|
-
function maxTierIdOf(address) external view returns (uint256) {
|
|
70
|
-
return _maxTierId;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function isTierRemoved(address, uint256 tierId) external view returns (bool) {
|
|
74
|
-
return _tiers[tierId].removed;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function tierOf(address, uint256 tierId, bool) external view returns (JB721Tier memory) {
|
|
78
|
-
TierData memory tier = _tiers[tierId];
|
|
79
|
-
return JB721Tier({
|
|
80
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
81
|
-
id: uint32(tierId),
|
|
82
|
-
price: tier.price,
|
|
83
|
-
remainingSupply: tier.supply,
|
|
84
|
-
initialSupply: tier.supply,
|
|
85
|
-
votingUnits: 0,
|
|
86
|
-
reserveFrequency: 0,
|
|
87
|
-
reserveBeneficiary: address(0),
|
|
88
|
-
encodedIPFSUri: tier.uri,
|
|
89
|
-
category: tier.category,
|
|
90
|
-
discountPercent: 0,
|
|
91
|
-
flags: JB721TierFlags({
|
|
92
|
-
allowOwnerMint: false,
|
|
93
|
-
transfersPausable: false,
|
|
94
|
-
cantBeRemoved: false,
|
|
95
|
-
cantIncreaseDiscountPercent: false,
|
|
96
|
-
cantBuyWithCredits: false
|
|
97
|
-
}),
|
|
98
|
-
splitPercent: 0,
|
|
99
|
-
resolvedUri: ""
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function addTier(JB721TierConfig calldata config) external returns (uint256 tierId) {
|
|
104
|
-
tierId = ++_maxTierId;
|
|
105
|
-
_tiers[tierId] = TierData({
|
|
106
|
-
uri: config.encodedIPFSUri,
|
|
107
|
-
price: config.price,
|
|
108
|
-
category: config.category,
|
|
109
|
-
supply: config.initialSupply,
|
|
110
|
-
removed: false
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
115
|
-
function setEncodedIPFSUriOf(uint256 tierId, bytes32 uri) external {
|
|
116
|
-
_tiers[tierId].uri = uri;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function tierUri(uint256 tierId) external view returns (bytes32) {
|
|
120
|
-
return _tiers[tierId].uri;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function maxTierId() external view returns (uint256) {
|
|
124
|
-
return _maxTierId;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
contract P12MockHook {
|
|
129
|
-
address internal _owner;
|
|
130
|
-
uint256 public immutable PROJECT_ID;
|
|
131
|
-
// forge-lint: disable-next-line(screaming-snake-case-immutable)
|
|
132
|
-
P12MockStore internal immutable _store;
|
|
133
|
-
|
|
134
|
-
constructor(address owner_, uint256 projectId_, P12MockStore store_) {
|
|
135
|
-
_owner = owner_;
|
|
136
|
-
PROJECT_ID = projectId_;
|
|
137
|
-
_store = store_;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function owner() external view returns (address) {
|
|
141
|
-
return _owner;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
145
|
-
function STORE() external view returns (IJB721TiersHookStore) {
|
|
146
|
-
return IJB721TiersHookStore(address(_store));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
150
|
-
function METADATA_ID_TARGET() external view returns (address) {
|
|
151
|
-
return address(this);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata) external {
|
|
155
|
-
for (uint256 i; i < tiersToAdd.length; i++) {
|
|
156
|
-
_store.addTier(tiersToAdd[i]);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function setMetadata(
|
|
161
|
-
string calldata,
|
|
162
|
-
string calldata,
|
|
163
|
-
string calldata,
|
|
164
|
-
string calldata,
|
|
165
|
-
address,
|
|
166
|
-
uint256 encodedIPFSUriTierId,
|
|
167
|
-
bytes32 encodedIPFSUri
|
|
168
|
-
)
|
|
169
|
-
external
|
|
170
|
-
{
|
|
171
|
-
require(msg.sender == _owner, "not owner");
|
|
172
|
-
_store.setEncodedIPFSUriOf(encodedIPFSUriTierId, encodedIPFSUri);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------
|
|
177
|
-
// Test contract
|
|
178
|
-
// ---------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
/// @notice Regression tests for Pass 12 audit fixes:
|
|
181
|
-
/// H-26: Metadata shadow — additionalPayMetadata with duplicate pay ID
|
|
182
|
-
/// M-42: URI cache desync — tier URI changed via setMetadata
|
|
183
|
-
contract Pass12FixesTest is Test {
|
|
184
|
-
bytes32 internal constant URI_A = keccak256("uri-a");
|
|
185
|
-
bytes32 internal constant URI_B = keccak256("uri-b");
|
|
186
|
-
|
|
187
|
-
JBPermissions internal permissions;
|
|
188
|
-
P12MockTerminal internal terminal;
|
|
189
|
-
P12MockDirectory internal directory;
|
|
190
|
-
P12MockStore internal store;
|
|
191
|
-
P12MockHook internal hook;
|
|
192
|
-
CTPublisher internal publisher;
|
|
193
|
-
|
|
194
|
-
address internal hookOwner = makeAddr("hookOwner");
|
|
195
|
-
address internal poster = makeAddr("poster");
|
|
196
|
-
uint256 internal constant FEE_PROJECT_ID = 1;
|
|
197
|
-
uint256 internal constant PROJECT_ID = 42;
|
|
198
|
-
|
|
199
|
-
function setUp() public {
|
|
200
|
-
permissions = new JBPermissions(address(0));
|
|
201
|
-
terminal = new P12MockTerminal();
|
|
202
|
-
directory = new P12MockDirectory(IJBTerminal(address(terminal)));
|
|
203
|
-
store = new P12MockStore();
|
|
204
|
-
hook = new P12MockHook(hookOwner, PROJECT_ID, store);
|
|
205
|
-
publisher = new CTPublisher(
|
|
206
|
-
IJBDirectory(address(directory)), IJBPermissions(address(permissions)), FEE_PROJECT_ID, address(0)
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
vm.deal(poster, 100 ether);
|
|
210
|
-
|
|
211
|
-
// Configure a category that allows posting.
|
|
212
|
-
CTAllowedPost[] memory allowedPosts = new CTAllowedPost[](1);
|
|
213
|
-
allowedPosts[0] = CTAllowedPost({
|
|
214
|
-
hook: address(hook),
|
|
215
|
-
category: 7,
|
|
216
|
-
minimumPrice: 1 ether,
|
|
217
|
-
minimumTotalSupply: 1,
|
|
218
|
-
maximumTotalSupply: 100,
|
|
219
|
-
maximumSplitPercent: 0,
|
|
220
|
-
allowedAddresses: new address[](0)
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
vm.prank(hookOwner);
|
|
224
|
-
publisher.configurePostingCriteriaFor(allowedPosts);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// -----------------------------------------------------------------------
|
|
228
|
-
// H-26: Metadata shadow — duplicate pay ID in additionalPayMetadata
|
|
229
|
-
// -----------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
/// @notice When additionalPayMetadata already contains an entry for the pay ID,
|
|
232
|
-
/// the fix should revert with CTPublisher_DuplicatePayMetadata.
|
|
233
|
-
function test_H26_fix_reverts_duplicate_metadata() public {
|
|
234
|
-
CTPost[] memory posts = new CTPost[](1);
|
|
235
|
-
posts[0] = CTPost({
|
|
236
|
-
encodedIPFSUri: URI_A,
|
|
237
|
-
totalSupply: 10,
|
|
238
|
-
price: 1 ether,
|
|
239
|
-
category: 7,
|
|
240
|
-
splitPercent: 0,
|
|
241
|
-
splits: new JBSplit[](0)
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// Build metadata that already contains the pay ID for this hook.
|
|
245
|
-
address metadataIdTarget = address(hook); // hook.METADATA_ID_TARGET() returns address(hook)
|
|
246
|
-
uint16[] memory forgedTierIds = new uint16[](1);
|
|
247
|
-
forgedTierIds[0] = 999; // Attacker's desired tier
|
|
248
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
249
|
-
bytes[] memory datas = new bytes[](1);
|
|
250
|
-
ids[0] = JBMetadataResolver.getId({purpose: "pay", target: metadataIdTarget});
|
|
251
|
-
datas[0] = abi.encode(true, forgedTierIds);
|
|
252
|
-
bytes memory shadowingMetadata = JBMetadataResolver.createMetadata(ids, datas);
|
|
253
|
-
|
|
254
|
-
vm.prank(poster);
|
|
255
|
-
vm.expectRevert(CTPublisher.CTPublisher_DuplicatePayMetadata.selector);
|
|
256
|
-
publisher.mintFrom{value: 1.05 ether}(
|
|
257
|
-
IJB721TiersHook(address(hook)), posts, poster, poster, shadowingMetadata, ""
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/// @notice Empty additionalPayMetadata should NOT revert.
|
|
262
|
-
function test_H26_fix_allows_empty_metadata() public {
|
|
263
|
-
CTPost[] memory posts = new CTPost[](1);
|
|
264
|
-
posts[0] = CTPost({
|
|
265
|
-
encodedIPFSUri: URI_A,
|
|
266
|
-
totalSupply: 10,
|
|
267
|
-
price: 1 ether,
|
|
268
|
-
category: 7,
|
|
269
|
-
splitPercent: 0,
|
|
270
|
-
splits: new JBSplit[](0)
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// Empty metadata — should succeed.
|
|
274
|
-
vm.prank(poster);
|
|
275
|
-
publisher.mintFrom{value: 1.05 ether}(IJB721TiersHook(address(hook)), posts, poster, poster, "", "");
|
|
276
|
-
|
|
277
|
-
assertEq(
|
|
278
|
-
publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "tier should be created with empty metadata"
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/// @notice additionalPayMetadata with a DIFFERENT ID (not the pay ID) should NOT revert.
|
|
283
|
-
function test_H26_fix_allows_unrelated_metadata() public {
|
|
284
|
-
CTPost[] memory posts = new CTPost[](1);
|
|
285
|
-
posts[0] = CTPost({
|
|
286
|
-
encodedIPFSUri: URI_A,
|
|
287
|
-
totalSupply: 10,
|
|
288
|
-
price: 1 ether,
|
|
289
|
-
category: 7,
|
|
290
|
-
splitPercent: 0,
|
|
291
|
-
splits: new JBSplit[](0)
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// Build metadata with a different purpose — should NOT trigger the check.
|
|
295
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
296
|
-
bytes[] memory datas = new bytes[](1);
|
|
297
|
-
ids[0] = JBMetadataResolver.getId({purpose: "unrelated", target: address(hook)});
|
|
298
|
-
datas[0] = abi.encode(uint256(42));
|
|
299
|
-
bytes memory unrelatedMetadata = JBMetadataResolver.createMetadata(ids, datas);
|
|
300
|
-
|
|
301
|
-
vm.prank(poster);
|
|
302
|
-
publisher.mintFrom{value: 1.05 ether}(
|
|
303
|
-
IJB721TiersHook(address(hook)), posts, poster, poster, unrelatedMetadata, ""
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
assertEq(
|
|
307
|
-
publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A),
|
|
308
|
-
1,
|
|
309
|
-
"tier should be created with unrelated metadata"
|
|
310
|
-
);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// -----------------------------------------------------------------------
|
|
314
|
-
// M-42: URI cache desync — tier URI changed via setMetadata
|
|
315
|
-
// -----------------------------------------------------------------------
|
|
316
|
-
|
|
317
|
-
/// @notice When a tier's URI is changed via setMetadata, the cache entry
|
|
318
|
-
/// (old URI -> tier ID) becomes stale. The fix should detect
|
|
319
|
-
/// the mismatch and clear the cache, creating a new tier.
|
|
320
|
-
function test_M42_fix_clears_stale_cache() public {
|
|
321
|
-
// Step 1: Publish URI_A — creates tier 1.
|
|
322
|
-
_publish(URI_A);
|
|
323
|
-
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A cached as tier 1");
|
|
324
|
-
|
|
325
|
-
// Step 2: Owner changes tier 1's URI from URI_A to URI_B via setMetadata.
|
|
326
|
-
vm.prank(hookOwner);
|
|
327
|
-
hook.setMetadata("", "", "", "", address(this), 1, URI_B);
|
|
328
|
-
|
|
329
|
-
// The publisher cache still maps URI_A -> tier 1, but tier 1 now has URI_B.
|
|
330
|
-
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "stale cache still maps URI_A -> tier 1");
|
|
331
|
-
|
|
332
|
-
// Step 3: Try to publish URI_A again. The fix should detect the mismatch
|
|
333
|
-
// (tier 1's actual URI is URI_B, not URI_A), clear the stale cache, and
|
|
334
|
-
// create a new tier 2 for URI_A.
|
|
335
|
-
_publish(URI_A);
|
|
336
|
-
|
|
337
|
-
assertEq(store.maxTierId(), 2, "new tier should be created for URI_A after cache invalidation");
|
|
338
|
-
assertEq(
|
|
339
|
-
publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 2, "URI_A should now map to tier 2 (fresh tier)"
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/// @notice When the tier was removed, the cache should still be cleared (existing behavior preserved).
|
|
344
|
-
function test_M42_fix_still_handles_removed_tiers() public {
|
|
345
|
-
// Use vm.mockCall to simulate isTierRemoved returning true for tier 1.
|
|
346
|
-
_publish(URI_A);
|
|
347
|
-
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A cached as tier 1");
|
|
348
|
-
|
|
349
|
-
// Mock isTierRemoved to return true for tier 1.
|
|
350
|
-
vm.mockCall(
|
|
351
|
-
address(store),
|
|
352
|
-
abi.encodeWithSelector(IJB721TiersHookStore.isTierRemoved.selector, address(hook), uint256(1)),
|
|
353
|
-
abi.encode(true)
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
// Publish URI_A again — should clear stale mapping and create tier 2.
|
|
357
|
-
_publish(URI_A);
|
|
358
|
-
|
|
359
|
-
assertEq(store.maxTierId(), 2, "new tier should be created after tier removal");
|
|
360
|
-
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 2, "URI_A should map to new tier 2");
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/// @notice When a cached tier's URI still matches, it should be reused (no regression).
|
|
364
|
-
function test_M42_fix_reuses_valid_cache() public {
|
|
365
|
-
// Publish URI_A — creates tier 1.
|
|
366
|
-
_publish(URI_A);
|
|
367
|
-
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A cached as tier 1");
|
|
368
|
-
|
|
369
|
-
// Publish URI_A again — URI still matches, should reuse tier 1 (no new tier created).
|
|
370
|
-
_publish(URI_A);
|
|
371
|
-
assertEq(store.maxTierId(), 1, "no new tier should be created for matching URI");
|
|
372
|
-
assertEq(publisher.tierIdForEncodedIPFSUriOf(address(hook), URI_A), 1, "URI_A still maps to tier 1");
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// -----------------------------------------------------------------------
|
|
376
|
-
// Helper
|
|
377
|
-
// -----------------------------------------------------------------------
|
|
378
|
-
|
|
379
|
-
function _publish(bytes32 uri) internal {
|
|
380
|
-
CTPost[] memory posts = new CTPost[](1);
|
|
381
|
-
posts[0] = CTPost({
|
|
382
|
-
encodedIPFSUri: uri, totalSupply: 10, price: 1 ether, category: 7, splitPercent: 0, splits: new JBSplit[](0)
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
vm.prank(poster);
|
|
386
|
-
publisher.mintFrom{value: 1.05 ether}(IJB721TiersHook(address(hook)), posts, poster, poster, "", "");
|
|
387
|
-
}
|
|
388
|
-
}
|