@croptop/core-v6 0.0.38 → 0.0.39
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 +52 -51
- package/src/interfaces/ICTDeployer.sol +2 -2
- 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
package/test/CTDeployer.t.sol
DELETED
|
@@ -1,616 +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 {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
8
|
-
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
9
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
10
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
11
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
|
-
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
13
|
-
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
14
|
-
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
15
|
-
import {IJBOwnable} from "@bananapus/ownable-v6/src/interfaces/IJBOwnable.sol";
|
|
16
|
-
import {IJB721Hook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Hook.sol";
|
|
17
|
-
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
18
|
-
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
19
|
-
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
20
|
-
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
21
|
-
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
22
|
-
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
23
|
-
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
24
|
-
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
25
|
-
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
26
|
-
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
27
|
-
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
28
|
-
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
29
|
-
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
30
|
-
|
|
31
|
-
import {CTDeployer} from "../src/CTDeployer.sol";
|
|
32
|
-
import {CTPublisher} from "../src/CTPublisher.sol";
|
|
33
|
-
import {ICTDeployer} from "../src/interfaces/ICTDeployer.sol";
|
|
34
|
-
import {ICTPublisher} from "../src/interfaces/ICTPublisher.sol";
|
|
35
|
-
import {CTDeployerAllowedPost} from "../src/structs/CTDeployerAllowedPost.sol";
|
|
36
|
-
import {CTProjectConfig} from "../src/structs/CTProjectConfig.sol";
|
|
37
|
-
import {CTSuckerDeploymentConfig} from "../src/structs/CTSuckerDeploymentConfig.sol";
|
|
38
|
-
|
|
39
|
-
// =============================================================================
|
|
40
|
-
// Mock data hook that returns successfully
|
|
41
|
-
// =============================================================================
|
|
42
|
-
contract MockDataHook is IJBRulesetDataHook {
|
|
43
|
-
uint256 public immutable WEIGHT;
|
|
44
|
-
uint256 public immutable TAX_RATE;
|
|
45
|
-
|
|
46
|
-
constructor(uint256 weight, uint256 taxRate) {
|
|
47
|
-
WEIGHT = weight;
|
|
48
|
-
TAX_RATE = taxRate;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
52
|
-
external
|
|
53
|
-
view
|
|
54
|
-
override
|
|
55
|
-
returns (
|
|
56
|
-
uint256 cashOutTaxRate,
|
|
57
|
-
uint256 cashOutCount,
|
|
58
|
-
uint256 totalSupply,
|
|
59
|
-
uint256 surplusValue,
|
|
60
|
-
JBCashOutHookSpecification[] memory hookSpecifications
|
|
61
|
-
)
|
|
62
|
-
{
|
|
63
|
-
return (TAX_RATE, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function beforePayRecordedWith(JBBeforePayRecordedContext calldata)
|
|
67
|
-
external
|
|
68
|
-
view
|
|
69
|
-
override
|
|
70
|
-
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
71
|
-
{
|
|
72
|
-
return (WEIGHT, hookSpecifications);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
|
|
76
|
-
return false;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
|
|
80
|
-
return interfaceId == type(IJBRulesetDataHook).interfaceId || interfaceId == type(IERC165).interfaceId;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/// @title TestCTDeployer
|
|
85
|
-
/// @notice Comprehensive unit tests for CTDeployer.
|
|
86
|
-
contract TestCTDeployer is Test {
|
|
87
|
-
CTDeployer deployer;
|
|
88
|
-
CTPublisher publisher;
|
|
89
|
-
|
|
90
|
-
IJBPermissions permissions = IJBPermissions(makeAddr("permissions"));
|
|
91
|
-
IJBProjects projects = IJBProjects(makeAddr("projects"));
|
|
92
|
-
IJB721TiersHookDeployer hookDeployer = IJB721TiersHookDeployer(makeAddr("hookDeployer"));
|
|
93
|
-
IJBSuckerRegistry suckerRegistry = IJBSuckerRegistry(makeAddr("suckerRegistry"));
|
|
94
|
-
IJBController controller = IJBController(makeAddr("controller"));
|
|
95
|
-
|
|
96
|
-
address owner = makeAddr("owner");
|
|
97
|
-
address unauthorized = makeAddr("unauthorized");
|
|
98
|
-
address hookAddr = makeAddr("hook");
|
|
99
|
-
address suckerAddr = makeAddr("sucker");
|
|
100
|
-
|
|
101
|
-
uint256 feeProjectId = 1;
|
|
102
|
-
uint256 projectCount = 5;
|
|
103
|
-
uint256 deployedProjectId = projectCount + 1; // 6
|
|
104
|
-
|
|
105
|
-
MockDataHook mockDataHook;
|
|
106
|
-
|
|
107
|
-
function setUp() public {
|
|
108
|
-
// Mock permissions.setPermissionsFor (called in CTDeployer constructor).
|
|
109
|
-
vm.mockCall(
|
|
110
|
-
address(permissions), abi.encodeWithSelector(IJBPermissions.setPermissionsFor.selector), abi.encode()
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
// Mock permissions.hasPermission to return true by default.
|
|
114
|
-
vm.mockCall(
|
|
115
|
-
address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true)
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Deploy publisher.
|
|
119
|
-
publisher = new CTPublisher(IJBDirectory(makeAddr("directory")), permissions, feeProjectId, address(0));
|
|
120
|
-
|
|
121
|
-
// Deploy the CTDeployer.
|
|
122
|
-
deployer = new CTDeployer(
|
|
123
|
-
permissions, projects, hookDeployer, ICTPublisher(address(publisher)), suckerRegistry, address(0)
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
// Deploy mock data hook (weight=1e24, taxRate=5000).
|
|
127
|
-
mockDataHook = new MockDataHook(1_000_000 * 1e18, 5000);
|
|
128
|
-
|
|
129
|
-
// Mock sucker registry: default false.
|
|
130
|
-
vm.mockCall(
|
|
131
|
-
address(suckerRegistry), abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector), abi.encode(false)
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
// Fund test accounts.
|
|
135
|
-
vm.deal(owner, 100 ether);
|
|
136
|
-
vm.deal(unauthorized, 100 ether);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
//*********************************************************************//
|
|
140
|
-
// --- deployProjectFor ---------------------------------------------- //
|
|
141
|
-
//*********************************************************************//
|
|
142
|
-
|
|
143
|
-
/// @notice Deploy a project and verify the hook address is returned and the data hook is stored.
|
|
144
|
-
function test_deployProjectFor_setsHookAndPublisher() public {
|
|
145
|
-
_mockDeployProjectInfra();
|
|
146
|
-
|
|
147
|
-
CTProjectConfig memory config = _defaultProjectConfig();
|
|
148
|
-
CTSuckerDeploymentConfig memory suckerConfig = _emptySuckerConfig();
|
|
149
|
-
|
|
150
|
-
(uint256 projectId, IJB721TiersHook hook) = deployer.deployProjectFor(owner, config, suckerConfig, controller);
|
|
151
|
-
|
|
152
|
-
assertEq(projectId, deployedProjectId, "project ID should match");
|
|
153
|
-
assertEq(address(hook), hookAddr, "hook address should match deployed hook");
|
|
154
|
-
assertEq(
|
|
155
|
-
address(deployer.dataHookOf(projectId)), hookAddr, "dataHookOf should be set to the deployed hook address"
|
|
156
|
-
);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/// @notice Verify that allowed posts from the config are forwarded to the publisher.
|
|
160
|
-
function test_deployProjectFor_configuresAllowedPosts() public {
|
|
161
|
-
_mockDeployProjectInfra();
|
|
162
|
-
|
|
163
|
-
CTDeployerAllowedPost[] memory allowedPosts = new CTDeployerAllowedPost[](1);
|
|
164
|
-
allowedPosts[0] = CTDeployerAllowedPost({
|
|
165
|
-
category: 5,
|
|
166
|
-
minimumPrice: 0.01 ether,
|
|
167
|
-
minimumTotalSupply: 1,
|
|
168
|
-
maximumTotalSupply: 100,
|
|
169
|
-
maximumSplitPercent: 0,
|
|
170
|
-
allowedAddresses: new address[](0)
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
CTProjectConfig memory config = CTProjectConfig({
|
|
174
|
-
terminalConfigurations: new JBTerminalConfig[](0),
|
|
175
|
-
projectUri: "https://croptop.test/",
|
|
176
|
-
allowedPosts: allowedPosts,
|
|
177
|
-
contractUri: "https://croptop.test/contract",
|
|
178
|
-
name: "TestCrop",
|
|
179
|
-
symbol: "TC",
|
|
180
|
-
salt: bytes32(0)
|
|
181
|
-
});
|
|
182
|
-
CTSuckerDeploymentConfig memory suckerConfig = _emptySuckerConfig();
|
|
183
|
-
|
|
184
|
-
// Mock the hook's owner() and PROJECT_ID() so the publisher's permission check passes.
|
|
185
|
-
// The CTDeployer is the hook's owner (it deployed the hook).
|
|
186
|
-
vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.owner.selector), abi.encode(address(deployer)));
|
|
187
|
-
vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
|
|
188
|
-
|
|
189
|
-
(uint256 projectId,) = deployer.deployProjectFor(owner, config, suckerConfig, controller);
|
|
190
|
-
assertEq(projectId, deployedProjectId, "project ID should match");
|
|
191
|
-
|
|
192
|
-
// Verify the allowance was set by reading it from the publisher.
|
|
193
|
-
(uint256 minPrice, uint256 minSupply, uint256 maxSupply,,) = publisher.allowanceFor(hookAddr, 5);
|
|
194
|
-
assertEq(minPrice, 0.01 ether, "minimum price should be configured");
|
|
195
|
-
assertEq(minSupply, 1, "minimum supply should be configured");
|
|
196
|
-
assertEq(maxSupply, 100, "maximum supply should be configured");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/// @notice Verify that deploying with suckerConfig.salt != 0 invokes the sucker registry.
|
|
200
|
-
function test_deployProjectFor_deploySuckersWhenSaltProvided() public {
|
|
201
|
-
_mockDeployProjectInfra();
|
|
202
|
-
|
|
203
|
-
CTSuckerDeploymentConfig memory suckerConfig = CTSuckerDeploymentConfig({
|
|
204
|
-
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(uint256(42))
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Mock the sucker registry deploySuckersFor call.
|
|
208
|
-
address[] memory mockSuckers = new address[](0);
|
|
209
|
-
vm.mockCall(
|
|
210
|
-
address(suckerRegistry),
|
|
211
|
-
abi.encodeWithSelector(IJBSuckerRegistry.deploySuckersFor.selector),
|
|
212
|
-
abi.encode(mockSuckers)
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
CTProjectConfig memory config = _defaultProjectConfig();
|
|
216
|
-
deployer.deployProjectFor(owner, config, suckerConfig, controller);
|
|
217
|
-
// If we got here without revert, the sucker registry was called successfully.
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/// @notice Deploying with a controller that has a different PROJECTS should revert.
|
|
221
|
-
function test_deployProjectFor_wrongControllerReverts() public {
|
|
222
|
-
// Mock controller.PROJECTS() to return a different address.
|
|
223
|
-
IJBProjects wrongProjects = IJBProjects(makeAddr("wrongProjects"));
|
|
224
|
-
vm.mockCall(
|
|
225
|
-
address(controller), abi.encodeWithSelector(IJBController.PROJECTS.selector), abi.encode(wrongProjects)
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
CTProjectConfig memory config = _defaultProjectConfig();
|
|
229
|
-
CTSuckerDeploymentConfig memory suckerConfig = _emptySuckerConfig();
|
|
230
|
-
|
|
231
|
-
vm.expectRevert();
|
|
232
|
-
deployer.deployProjectFor(owner, config, suckerConfig, controller);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/// @notice The project NFT is transferred to the owner after deployment.
|
|
236
|
-
function test_deployProjectFor_transfersProjectToOwner() public {
|
|
237
|
-
_mockDeployProjectInfra();
|
|
238
|
-
|
|
239
|
-
CTProjectConfig memory config = _defaultProjectConfig();
|
|
240
|
-
CTSuckerDeploymentConfig memory suckerConfig = _emptySuckerConfig();
|
|
241
|
-
|
|
242
|
-
// Mock the transferFrom call - expect it to be called with (deployer, owner, projectId).
|
|
243
|
-
vm.mockCall(address(projects), abi.encodeWithSelector(IERC721.transferFrom.selector), abi.encode());
|
|
244
|
-
|
|
245
|
-
// We expect the transferFrom to be called. If the mock is not matched, it will revert.
|
|
246
|
-
deployer.deployProjectFor(owner, config, suckerConfig, controller);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
//*********************************************************************//
|
|
250
|
-
// --- deploySuckersFor ---------------------------------------------- //
|
|
251
|
-
//*********************************************************************//
|
|
252
|
-
|
|
253
|
-
/// @notice deploySuckersFor forwards to the sucker registry with correct permission checks.
|
|
254
|
-
function test_deploySuckersFor_forwardsToRegistry() public {
|
|
255
|
-
// Mock projects.ownerOf to return `owner`.
|
|
256
|
-
vm.mockCall(
|
|
257
|
-
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(owner)
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
// Mock the sucker registry deploySuckersFor call.
|
|
261
|
-
address[] memory mockSuckers = new address[](1);
|
|
262
|
-
mockSuckers[0] = suckerAddr;
|
|
263
|
-
vm.mockCall(
|
|
264
|
-
address(suckerRegistry),
|
|
265
|
-
abi.encodeWithSelector(IJBSuckerRegistry.deploySuckersFor.selector),
|
|
266
|
-
abi.encode(mockSuckers)
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
CTSuckerDeploymentConfig memory suckerConfig = CTSuckerDeploymentConfig({
|
|
270
|
-
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(uint256(99))
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
vm.prank(owner);
|
|
274
|
-
address[] memory suckers = deployer.deploySuckersFor(deployedProjectId, suckerConfig);
|
|
275
|
-
assertEq(suckers.length, 1, "should return 1 sucker");
|
|
276
|
-
assertEq(suckers[0], suckerAddr, "sucker address should match");
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/// @notice deploySuckersFor reverts when caller is not the owner and lacks permission.
|
|
280
|
-
function test_deploySuckersFor_nonOwner_reverts() public {
|
|
281
|
-
// Mock projects.ownerOf to return `owner`.
|
|
282
|
-
vm.mockCall(
|
|
283
|
-
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(owner)
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
// Mock permissions.hasPermission to return false for unauthorized.
|
|
287
|
-
vm.mockCall(
|
|
288
|
-
address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false)
|
|
289
|
-
);
|
|
290
|
-
|
|
291
|
-
CTSuckerDeploymentConfig memory suckerConfig = CTSuckerDeploymentConfig({
|
|
292
|
-
deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(uint256(99))
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
vm.prank(unauthorized);
|
|
296
|
-
vm.expectRevert(
|
|
297
|
-
abi.encodeWithSelector(
|
|
298
|
-
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
299
|
-
owner,
|
|
300
|
-
unauthorized,
|
|
301
|
-
deployedProjectId,
|
|
302
|
-
JBPermissionIds.DEPLOY_SUCKERS
|
|
303
|
-
)
|
|
304
|
-
);
|
|
305
|
-
deployer.deploySuckersFor(deployedProjectId, suckerConfig);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
//*********************************************************************//
|
|
309
|
-
// --- claimCollectionOwnershipOf ------------------------------------ //
|
|
310
|
-
//*********************************************************************//
|
|
311
|
-
|
|
312
|
-
/// @notice claimCollectionOwnershipOf transfers hook ownership to the project.
|
|
313
|
-
function test_claimCollectionOwnershipOf_transfersOwnership() public {
|
|
314
|
-
// Mock hook.PROJECT_ID() to return deployedProjectId.
|
|
315
|
-
vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
|
|
316
|
-
|
|
317
|
-
// Mock PROJECTS.ownerOf(deployedProjectId) to return owner.
|
|
318
|
-
vm.mockCall(
|
|
319
|
-
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(owner)
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
// Mock JBOwnable.transferOwnershipToProject(projectId).
|
|
323
|
-
vm.mockCall(hookAddr, abi.encodeWithSelector(IJBOwnable.transferOwnershipToProject.selector), abi.encode());
|
|
324
|
-
|
|
325
|
-
vm.prank(owner);
|
|
326
|
-
deployer.claimCollectionOwnershipOf(IJB721TiersHook(hookAddr));
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/// @notice claimCollectionOwnershipOf reverts when called by a non-owner.
|
|
330
|
-
function test_claimCollectionOwnershipOf_nonOwner_reverts() public {
|
|
331
|
-
// Mock hook.PROJECT_ID() to return deployedProjectId.
|
|
332
|
-
vm.mockCall(hookAddr, abi.encodeWithSelector(IJB721Hook.PROJECT_ID.selector), abi.encode(deployedProjectId));
|
|
333
|
-
|
|
334
|
-
// Mock PROJECTS.ownerOf(deployedProjectId) to return owner (not unauthorized).
|
|
335
|
-
vm.mockCall(
|
|
336
|
-
address(projects), abi.encodeWithSelector(IERC721.ownerOf.selector, deployedProjectId), abi.encode(owner)
|
|
337
|
-
);
|
|
338
|
-
|
|
339
|
-
vm.prank(unauthorized);
|
|
340
|
-
vm.expectRevert(
|
|
341
|
-
abi.encodeWithSelector(
|
|
342
|
-
CTDeployer.CTDeployer_NotOwnerOfProject.selector, deployedProjectId, hookAddr, unauthorized
|
|
343
|
-
)
|
|
344
|
-
);
|
|
345
|
-
deployer.claimCollectionOwnershipOf(IJB721TiersHook(hookAddr));
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
//*********************************************************************//
|
|
349
|
-
// --- hasMintPermissionFor ------------------------------------------ //
|
|
350
|
-
//*********************************************************************//
|
|
351
|
-
|
|
352
|
-
/// @notice hasMintPermissionFor returns true for a registered sucker.
|
|
353
|
-
function test_hasMintPermissionFor_returnsCorrectly() public {
|
|
354
|
-
JBRuleset memory ruleset;
|
|
355
|
-
|
|
356
|
-
// Non-sucker should return false.
|
|
357
|
-
bool allowed = deployer.hasMintPermissionFor(deployedProjectId, ruleset, unauthorized);
|
|
358
|
-
assertFalse(allowed, "non-sucker should not have mint permission");
|
|
359
|
-
|
|
360
|
-
// Register suckerAddr as a valid sucker for the project.
|
|
361
|
-
vm.mockCall(
|
|
362
|
-
address(suckerRegistry),
|
|
363
|
-
abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector, deployedProjectId, suckerAddr),
|
|
364
|
-
abi.encode(true)
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
allowed = deployer.hasMintPermissionFor(deployedProjectId, ruleset, suckerAddr);
|
|
368
|
-
assertTrue(allowed, "valid sucker should have mint permission");
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/// @notice hasMintPermissionFor returns false for a sucker registered to a different project.
|
|
372
|
-
function test_hasMintPermissionFor_wrongProjectReturnsFalse() public {
|
|
373
|
-
// Register suckerAddr for project 99.
|
|
374
|
-
vm.mockCall(
|
|
375
|
-
address(suckerRegistry),
|
|
376
|
-
abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector, 99, suckerAddr),
|
|
377
|
-
abi.encode(true)
|
|
378
|
-
);
|
|
379
|
-
// Default mock for deployedProjectId returns false.
|
|
380
|
-
|
|
381
|
-
JBRuleset memory ruleset;
|
|
382
|
-
bool allowed = deployer.hasMintPermissionFor(deployedProjectId, ruleset, suckerAddr);
|
|
383
|
-
assertFalse(allowed, "sucker for different project should not have mint permission");
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
//*********************************************************************//
|
|
387
|
-
// --- supportsInterface --------------------------------------------- //
|
|
388
|
-
//*********************************************************************//
|
|
389
|
-
|
|
390
|
-
/// @notice supportsInterface returns true for all declared interfaces.
|
|
391
|
-
function test_supportsInterface_correctInterfaces() public {
|
|
392
|
-
assertTrue(deployer.supportsInterface(type(ICTDeployer).interfaceId), "should support ICTDeployer");
|
|
393
|
-
assertTrue(
|
|
394
|
-
deployer.supportsInterface(type(IJBRulesetDataHook).interfaceId), "should support IJBRulesetDataHook"
|
|
395
|
-
);
|
|
396
|
-
assertTrue(deployer.supportsInterface(type(IERC721Receiver).interfaceId), "should support IERC721Receiver");
|
|
397
|
-
assertFalse(deployer.supportsInterface(bytes4(0xdeadbeef)), "should not support random interface");
|
|
398
|
-
assertFalse(deployer.supportsInterface(bytes4(0)), "should not support zero interface");
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
//*********************************************************************//
|
|
402
|
-
// --- beforePayRecordedWith ----------------------------------------- //
|
|
403
|
-
//*********************************************************************//
|
|
404
|
-
|
|
405
|
-
/// @notice beforePayRecordedWith forwards the call to the stored data hook.
|
|
406
|
-
function test_beforePayRecordedWith_forwardsToHook() public {
|
|
407
|
-
// Set the data hook for the project.
|
|
408
|
-
_setDataHookForProject(deployedProjectId, IJBRulesetDataHook(address(mockDataHook)));
|
|
409
|
-
|
|
410
|
-
JBBeforePayRecordedContext memory context = _buildPayContext(deployedProjectId);
|
|
411
|
-
|
|
412
|
-
(uint256 weight,) = deployer.beforePayRecordedWith(context);
|
|
413
|
-
assertEq(weight, 1_000_000 * 1e18, "weight should be forwarded from mock data hook");
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/// @notice beforePayRecordedWith reverts when no data hook is set.
|
|
417
|
-
function test_beforePayRecordedWith_returnsDefaultsWhenNoDataHook() public {
|
|
418
|
-
// dataHookOf[999] is address(0) by default.
|
|
419
|
-
JBBeforePayRecordedContext memory context = _buildPayContext(999);
|
|
420
|
-
|
|
421
|
-
(uint256 weight, JBPayHookSpecification[] memory specs) = deployer.beforePayRecordedWith(context);
|
|
422
|
-
|
|
423
|
-
assertEq(weight, context.weight, "weight should be returned as-is from context");
|
|
424
|
-
assertEq(specs.length, 0, "hookSpecifications should be empty");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
//*********************************************************************//
|
|
428
|
-
// --- beforeCashOutRecordedWith ------------------------------------- //
|
|
429
|
-
//*********************************************************************//
|
|
430
|
-
|
|
431
|
-
/// @notice beforeCashOutRecordedWith forwards the call to the stored data hook for non-suckers.
|
|
432
|
-
function test_beforeCashOutRecordedWith_forwardsToHook() public {
|
|
433
|
-
_setDataHookForProject(deployedProjectId, IJBRulesetDataHook(address(mockDataHook)));
|
|
434
|
-
|
|
435
|
-
JBBeforeCashOutRecordedContext memory context =
|
|
436
|
-
_buildCashOutContext(deployedProjectId, unauthorized, 100e18, 1000e18);
|
|
437
|
-
|
|
438
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
439
|
-
|
|
440
|
-
assertEq(taxRate, 5000, "tax rate should come from data hook");
|
|
441
|
-
assertEq(cashOutCount, 100e18, "cashOutCount should be forwarded");
|
|
442
|
-
assertEq(totalSupply, 1000e18, "totalSupply should be forwarded");
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/// @notice Suckers get 0% tax rate and bypass the data hook.
|
|
446
|
-
function test_beforeCashOutRecordedWith_suckerGetsZeroTax() public {
|
|
447
|
-
_setDataHookForProject(deployedProjectId, IJBRulesetDataHook(address(mockDataHook)));
|
|
448
|
-
|
|
449
|
-
// Register suckerAddr for this project.
|
|
450
|
-
vm.mockCall(
|
|
451
|
-
address(suckerRegistry),
|
|
452
|
-
abi.encodeWithSelector(IJBSuckerRegistry.isSuckerOf.selector, deployedProjectId, suckerAddr),
|
|
453
|
-
abi.encode(true)
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
JBBeforeCashOutRecordedContext memory context =
|
|
457
|
-
_buildCashOutContext(deployedProjectId, suckerAddr, 100e18, 1000e18);
|
|
458
|
-
|
|
459
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
460
|
-
|
|
461
|
-
assertEq(taxRate, 0, "sucker should get 0% tax rate");
|
|
462
|
-
assertEq(cashOutCount, 100e18, "cashOutCount should pass through");
|
|
463
|
-
assertEq(totalSupply, 1000e18, "totalSupply should pass through");
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/// @notice beforeCashOutRecordedWith returns defaults when no data hook is set and holder is not a sucker.
|
|
467
|
-
function test_beforeCashOutRecordedWith_returnsDefaultsWhenNoDataHook() public {
|
|
468
|
-
JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(999, unauthorized, 100e18, 1000e18);
|
|
469
|
-
|
|
470
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,, JBCashOutHookSpecification[] memory specs) =
|
|
471
|
-
deployer.beforeCashOutRecordedWith(context);
|
|
472
|
-
|
|
473
|
-
assertEq(taxRate, context.cashOutTaxRate, "cashOutTaxRate should be returned as-is from context");
|
|
474
|
-
assertEq(cashOutCount, context.cashOutCount, "cashOutCount should be returned as-is from context");
|
|
475
|
-
assertEq(totalSupply, context.totalSupply, "totalSupply should be returned as-is from context");
|
|
476
|
-
assertEq(specs.length, 0, "hookSpecifications should be empty");
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
//*********************************************************************//
|
|
480
|
-
// --- onERC721Received ---------------------------------------------- //
|
|
481
|
-
//*********************************************************************//
|
|
482
|
-
|
|
483
|
-
/// @notice onERC721Received accepts mints from the PROJECTS contract.
|
|
484
|
-
function test_onERC721Received_acceptsMintFromProjects() public {
|
|
485
|
-
vm.prank(address(projects));
|
|
486
|
-
bytes4 result = deployer.onERC721Received(address(0), address(0), 1, "");
|
|
487
|
-
assertEq(result, IERC721Receiver.onERC721Received.selector, "should return the ERC721Received selector");
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/// @notice onERC721Received reverts when called by a non-PROJECTS contract.
|
|
491
|
-
function test_onERC721Received_revertsWhenNotProjects() public {
|
|
492
|
-
vm.prank(unauthorized);
|
|
493
|
-
vm.expectRevert();
|
|
494
|
-
deployer.onERC721Received(address(0), address(0), 1, "");
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/// @notice onERC721Received reverts when the `from` address is not address(0) (not a mint).
|
|
498
|
-
function test_onERC721Received_revertsWhenNotMint() public {
|
|
499
|
-
vm.prank(address(projects));
|
|
500
|
-
vm.expectRevert();
|
|
501
|
-
deployer.onERC721Received(address(0), owner, 1, "");
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
//*********************************************************************//
|
|
505
|
-
// --- Constructor immutables ---------------------------------------- //
|
|
506
|
-
//*********************************************************************//
|
|
507
|
-
|
|
508
|
-
/// @notice Verify that constructor sets all immutable values correctly.
|
|
509
|
-
function test_constructor_setsImmutables() public {
|
|
510
|
-
assertEq(address(deployer.PROJECTS()), address(projects), "PROJECTS should be set");
|
|
511
|
-
assertEq(address(deployer.DEPLOYER()), address(hookDeployer), "DEPLOYER should be set");
|
|
512
|
-
assertEq(address(deployer.PUBLISHER()), address(publisher), "PUBLISHER should be set");
|
|
513
|
-
assertEq(address(deployer.SUCKER_REGISTRY()), address(suckerRegistry), "SUCKER_REGISTRY should be set");
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
//*********************************************************************//
|
|
517
|
-
// --- Internal Helpers ---------------------------------------------- //
|
|
518
|
-
//*********************************************************************//
|
|
519
|
-
|
|
520
|
-
/// @dev Build a default CTProjectConfig with no allowed posts or terminals.
|
|
521
|
-
function _defaultProjectConfig() internal pure returns (CTProjectConfig memory) {
|
|
522
|
-
return CTProjectConfig({
|
|
523
|
-
terminalConfigurations: new JBTerminalConfig[](0),
|
|
524
|
-
projectUri: "https://croptop.test/",
|
|
525
|
-
allowedPosts: new CTDeployerAllowedPost[](0),
|
|
526
|
-
contractUri: "https://croptop.test/contract",
|
|
527
|
-
name: "TestCrop",
|
|
528
|
-
symbol: "TC",
|
|
529
|
-
salt: bytes32(0)
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/// @dev Build an empty CTSuckerDeploymentConfig (no suckers).
|
|
534
|
-
function _emptySuckerConfig() internal pure returns (CTSuckerDeploymentConfig memory) {
|
|
535
|
-
return CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/// @dev Set up all the mocks needed for a successful deployProjectFor call.
|
|
539
|
-
function _mockDeployProjectInfra() internal {
|
|
540
|
-
// Mock controller.PROJECTS() to return the same projects contract.
|
|
541
|
-
vm.mockCall(address(controller), abi.encodeWithSelector(IJBController.PROJECTS.selector), abi.encode(projects));
|
|
542
|
-
|
|
543
|
-
// Mock projects.count() to return the current count.
|
|
544
|
-
vm.mockCall(address(projects), abi.encodeWithSelector(IJBProjects.count.selector), abi.encode(projectCount));
|
|
545
|
-
|
|
546
|
-
// Mock hookDeployer.deployHookFor to return the hook address.
|
|
547
|
-
vm.mockCall(
|
|
548
|
-
address(hookDeployer),
|
|
549
|
-
abi.encodeWithSelector(IJB721TiersHookDeployer.deployHookFor.selector),
|
|
550
|
-
abi.encode(IJB721TiersHook(hookAddr))
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
// Mock controller.launchProjectFor to return the expected project ID.
|
|
554
|
-
vm.mockCall(
|
|
555
|
-
address(controller),
|
|
556
|
-
abi.encodeWithSelector(IJBController.launchProjectFor.selector),
|
|
557
|
-
abi.encode(deployedProjectId)
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
// Mock projects.transferFrom (ERC721 transfer of project NFT to owner).
|
|
561
|
-
vm.mockCall(address(projects), abi.encodeWithSelector(IERC721.transferFrom.selector), abi.encode());
|
|
562
|
-
|
|
563
|
-
// Mock permissions.setPermissionsFor (called for owner permissions after deployment).
|
|
564
|
-
vm.mockCall(
|
|
565
|
-
address(permissions), abi.encodeWithSelector(IJBPermissions.setPermissionsFor.selector), abi.encode()
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/// @dev Use vm.store to set the dataHookOf mapping directly.
|
|
570
|
-
/// dataHookOf is the first non-immutable storage variable in CTDeployer, at slot 0.
|
|
571
|
-
function _setDataHookForProject(uint256 projectId, IJBRulesetDataHook hook) internal {
|
|
572
|
-
bytes32 slot = keccak256(abi.encode(projectId, uint256(0)));
|
|
573
|
-
vm.store(address(deployer), slot, bytes32(uint256(uint160(address(hook)))));
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
/// @dev Build a minimal JBBeforePayRecordedContext.
|
|
577
|
-
function _buildPayContext(uint256 projectId) internal pure returns (JBBeforePayRecordedContext memory) {
|
|
578
|
-
return JBBeforePayRecordedContext({
|
|
579
|
-
terminal: address(0),
|
|
580
|
-
payer: address(0),
|
|
581
|
-
amount: JBTokenAmount({token: address(0), decimals: 18, currency: 0, value: 1 ether}),
|
|
582
|
-
projectId: projectId,
|
|
583
|
-
rulesetId: 1,
|
|
584
|
-
beneficiary: address(0),
|
|
585
|
-
weight: 1_000_000 * 1e18,
|
|
586
|
-
reservedPercent: 0,
|
|
587
|
-
metadata: ""
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
/// @dev Build a minimal JBBeforeCashOutRecordedContext.
|
|
592
|
-
function _buildCashOutContext(
|
|
593
|
-
uint256 projectId,
|
|
594
|
-
address holder,
|
|
595
|
-
uint256 cashOutCount,
|
|
596
|
-
uint256 totalSupply
|
|
597
|
-
)
|
|
598
|
-
internal
|
|
599
|
-
pure
|
|
600
|
-
returns (JBBeforeCashOutRecordedContext memory)
|
|
601
|
-
{
|
|
602
|
-
return JBBeforeCashOutRecordedContext({
|
|
603
|
-
terminal: address(0),
|
|
604
|
-
holder: holder,
|
|
605
|
-
projectId: projectId,
|
|
606
|
-
rulesetId: 1,
|
|
607
|
-
cashOutCount: cashOutCount,
|
|
608
|
-
totalSupply: totalSupply,
|
|
609
|
-
surplus: JBTokenAmount({token: address(0), decimals: 18, currency: 0, value: 1 ether}),
|
|
610
|
-
useTotalSurplus: false,
|
|
611
|
-
cashOutTaxRate: 10_000,
|
|
612
|
-
beneficiaryIsFeeless: false,
|
|
613
|
-
metadata: ""
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
}
|