@croptop/core-v6 0.0.31 → 0.0.33
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 -0
- package/package.json +8 -8
- package/src/CTDeployer.sol +116 -107
- package/src/CTPublisher.sol +136 -136
- package/test/CTDeployer.t.sol +5 -4
- package/test/Fork.t.sol +6 -3
- package/test/TestAuditGaps.sol +9 -8
- package/test/audit/CodexNemesisPoCs.t.sol +263 -0
- package/test/fork/PublishFork.t.sol +6 -3
package/README.md
CHANGED
|
@@ -90,4 +90,6 @@ script/
|
|
|
90
90
|
- fee routing depends on the designated fee project remaining correctly configured; if its terminal rejects payments,
|
|
91
91
|
Croptop refunds the fee to `_msgSender()` instead of trapping ETH in `CTPublisher`
|
|
92
92
|
- burn-lock ownership is intentionally irreversible and should only be used when immutability is desired
|
|
93
|
+
- after burn-locking into `CTProjectOwner`, the previous owner no longer holds the project NFT directly; control is
|
|
94
|
+
intentionally mediated through Croptop's owner helper and hook-admin surface instead of remaining a plain owner EOA
|
|
93
95
|
- duplicate-content and stale-tier edge cases are guarded by tests, but integrations should still treat metadata reuse carefully
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@croptop/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.33",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,18 +16,18 @@
|
|
|
16
16
|
"artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'croptop-core-v5'"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@bananapus/721-hook-v6": "^0.0.
|
|
20
|
-
"@bananapus/buyback-hook-v6": "^0.0.
|
|
21
|
-
"@bananapus/core-v6": "^0.0.
|
|
19
|
+
"@bananapus/721-hook-v6": "^0.0.33",
|
|
20
|
+
"@bananapus/buyback-hook-v6": "^0.0.27",
|
|
21
|
+
"@bananapus/core-v6": "^0.0.34",
|
|
22
22
|
"@bananapus/ownable-v6": "^0.0.17",
|
|
23
|
-
"@bananapus/permission-ids-v6": "^0.0.
|
|
23
|
+
"@bananapus/permission-ids-v6": "^0.0.17",
|
|
24
24
|
"@bananapus/router-terminal-v6": "^0.0.26",
|
|
25
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
25
|
+
"@bananapus/suckers-v6": "^0.0.25",
|
|
26
26
|
"@openzeppelin/contracts": "^5.6.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@bananapus/address-registry-v6": "^0.0.17",
|
|
30
|
-
"@rev-net/core-v6": "^0.0.
|
|
30
|
+
"@rev-net/core-v6": "^0.0.31",
|
|
31
31
|
"@sphinx-labs/plugins": "^0.33.3"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
package/src/CTDeployer.sol
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.28;
|
|
3
3
|
|
|
4
|
-
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
5
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
6
|
-
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
7
4
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
8
5
|
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
9
6
|
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
@@ -28,6 +25,9 @@ import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.so
|
|
|
28
25
|
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
29
26
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
30
27
|
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
28
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
29
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
30
|
+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
31
31
|
|
|
32
32
|
import {ICTDeployer} from "./interfaces/ICTDeployer.sol";
|
|
33
33
|
import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
|
|
@@ -117,109 +117,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
117
117
|
PERMISSIONS.setPermissionsFor({account: address(this), permissionsData: permissionData});
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
//*********************************************************************//
|
|
121
|
-
// ------------------------- external views -------------------------- //
|
|
122
|
-
//*********************************************************************//
|
|
123
|
-
|
|
124
|
-
/// @notice Allow cash outs from suckers without a tax.
|
|
125
|
-
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
126
|
-
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
127
|
-
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
|
|
128
|
-
/// out.
|
|
129
|
-
/// @return cashOutCount The number of project tokens that are cashed out.
|
|
130
|
-
/// @return totalSupply The total project token supply.
|
|
131
|
-
/// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
|
|
132
|
-
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
133
|
-
external
|
|
134
|
-
view
|
|
135
|
-
override
|
|
136
|
-
returns (
|
|
137
|
-
uint256 cashOutTaxRate,
|
|
138
|
-
uint256 cashOutCount,
|
|
139
|
-
uint256 totalSupply,
|
|
140
|
-
JBCashOutHookSpecification[] memory hookSpecifications
|
|
141
|
-
)
|
|
142
|
-
{
|
|
143
|
-
// If the cash out is from a sucker, return the full cash out amount without taxes or fees.
|
|
144
|
-
if (SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})) {
|
|
145
|
-
return (0, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// If the ruleset has a data hook, forward the call to the datahook.
|
|
149
|
-
IJBRulesetDataHook hook = dataHookOf[context.projectId];
|
|
150
|
-
if (address(hook) == address(0)) {
|
|
151
|
-
return (context.cashOutTaxRate, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
152
|
-
}
|
|
153
|
-
// slither-disable-next-line unused-return
|
|
154
|
-
return hook.beforeCashOutRecordedWith(context);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/// @notice Forward the call to the original data hook.
|
|
158
|
-
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
|
|
159
|
-
/// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
|
|
160
|
-
/// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
|
|
161
|
-
/// tokens get minted by a payment.
|
|
162
|
-
/// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
|
|
163
|
-
/// into the project. Useful for automatically routing funds from a treasury as payments come in.
|
|
164
|
-
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
165
|
-
external
|
|
166
|
-
view
|
|
167
|
-
override
|
|
168
|
-
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
169
|
-
{
|
|
170
|
-
// Forward the call to the data hook.
|
|
171
|
-
IJBRulesetDataHook hook = dataHookOf[context.projectId];
|
|
172
|
-
if (address(hook) == address(0)) {
|
|
173
|
-
return (context.weight, hookSpecifications);
|
|
174
|
-
}
|
|
175
|
-
// slither-disable-next-line unused-return
|
|
176
|
-
return hook.beforePayRecordedWith(context);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// @notice A flag indicating whether an address has permission to mint a project's tokens on-demand.
|
|
180
|
-
/// @dev A project's data hook can allow any address to mint its tokens.
|
|
181
|
-
/// @param projectId The ID of the project whose token can be minted.
|
|
182
|
-
/// @param addr The address to check the token minting permission of.
|
|
183
|
-
/// @return flag A flag indicating whether the address has permission to mint the project's tokens on-demand.
|
|
184
|
-
function hasMintPermissionFor(uint256 projectId, JBRuleset memory, address addr) external view returns (bool flag) {
|
|
185
|
-
// If the address is a sucker for this project.
|
|
186
|
-
return SUCKER_REGISTRY.isSuckerOf({projectId: projectId, addr: addr});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/// @dev Make sure only mints can be received.
|
|
190
|
-
function onERC721Received(
|
|
191
|
-
address operator,
|
|
192
|
-
address from,
|
|
193
|
-
uint256 tokenId,
|
|
194
|
-
bytes calldata data
|
|
195
|
-
)
|
|
196
|
-
external
|
|
197
|
-
view
|
|
198
|
-
returns (bytes4)
|
|
199
|
-
{
|
|
200
|
-
data;
|
|
201
|
-
tokenId;
|
|
202
|
-
operator;
|
|
203
|
-
|
|
204
|
-
// Make sure the 721 received is the JBProjects contract.
|
|
205
|
-
if (msg.sender != address(PROJECTS)) revert();
|
|
206
|
-
// Make sure the 721 is being received as a mint.
|
|
207
|
-
if (from != address(0)) revert();
|
|
208
|
-
return IERC721Receiver.onERC721Received.selector;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
//*********************************************************************//
|
|
212
|
-
// -------------------------- public views --------------------------- //
|
|
213
|
-
//*********************************************************************//
|
|
214
|
-
|
|
215
|
-
/// @notice Indicates if this contract adheres to the specified interface.
|
|
216
|
-
/// @dev See `IERC165.supportsInterface`.
|
|
217
|
-
/// @return A flag indicating if the provided interface ID is supported.
|
|
218
|
-
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
219
|
-
return interfaceId == type(ICTDeployer).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
220
|
-
|| interfaceId == type(IERC721Receiver).interfaceId;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
120
|
//*********************************************************************//
|
|
224
121
|
// ---------------------- external transactions ---------------------- //
|
|
225
122
|
//*********************************************************************//
|
|
@@ -389,6 +286,118 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
389
286
|
});
|
|
390
287
|
}
|
|
391
288
|
|
|
289
|
+
//*********************************************************************//
|
|
290
|
+
// ------------------------- external views -------------------------- //
|
|
291
|
+
//*********************************************************************//
|
|
292
|
+
|
|
293
|
+
/// @notice Allow cash outs from suckers without a tax.
|
|
294
|
+
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
295
|
+
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
296
|
+
/// @return cashOutTaxRate The cash out tax rate, which influences the amount of terminal tokens which get cashed
|
|
297
|
+
/// out.
|
|
298
|
+
/// @return cashOutCount The number of project tokens that are cashed out.
|
|
299
|
+
/// @return totalSupply The total project token supply.
|
|
300
|
+
/// @return surplusValue The surplus value to use for the bonding curve calculation.
|
|
301
|
+
/// @return hookSpecifications The amount of funds and the data to send to cash out hooks (this contract).
|
|
302
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
303
|
+
external
|
|
304
|
+
view
|
|
305
|
+
override
|
|
306
|
+
returns (
|
|
307
|
+
uint256 cashOutTaxRate,
|
|
308
|
+
uint256 cashOutCount,
|
|
309
|
+
uint256 totalSupply,
|
|
310
|
+
uint256 surplusValue,
|
|
311
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
312
|
+
)
|
|
313
|
+
{
|
|
314
|
+
// If the cash out is from a sucker, return the full cash out amount without taxes or fees.
|
|
315
|
+
if (SUCKER_REGISTRY.isSuckerOf({projectId: context.projectId, addr: context.holder})) {
|
|
316
|
+
return (0, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// If the ruleset has a data hook, forward the call to the datahook.
|
|
320
|
+
IJBRulesetDataHook hook = dataHookOf[context.projectId];
|
|
321
|
+
if (address(hook) == address(0)) {
|
|
322
|
+
return (
|
|
323
|
+
context.cashOutTaxRate,
|
|
324
|
+
context.cashOutCount,
|
|
325
|
+
context.totalSupply,
|
|
326
|
+
context.surplus.value,
|
|
327
|
+
hookSpecifications
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
// slither-disable-next-line unused-return
|
|
331
|
+
return hook.beforeCashOutRecordedWith(context);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/// @notice Forward the call to the original data hook.
|
|
335
|
+
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
|
|
336
|
+
/// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
|
|
337
|
+
/// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
|
|
338
|
+
/// tokens get minted by a payment.
|
|
339
|
+
/// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
|
|
340
|
+
/// into the project. Useful for automatically routing funds from a treasury as payments come in.
|
|
341
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
342
|
+
external
|
|
343
|
+
view
|
|
344
|
+
override
|
|
345
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
346
|
+
{
|
|
347
|
+
// Forward the call to the data hook.
|
|
348
|
+
IJBRulesetDataHook hook = dataHookOf[context.projectId];
|
|
349
|
+
if (address(hook) == address(0)) {
|
|
350
|
+
return (context.weight, hookSpecifications);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// slither-disable-next-line unused-return
|
|
354
|
+
return hook.beforePayRecordedWith(context);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/// @notice A flag indicating whether an address has permission to mint a project's tokens on-demand.
|
|
358
|
+
/// @dev A project's data hook can allow any address to mint its tokens.
|
|
359
|
+
/// @param projectId The ID of the project whose token can be minted.
|
|
360
|
+
/// @param addr The address to check the token minting permission of.
|
|
361
|
+
/// @return flag A flag indicating whether the address has permission to mint the project's tokens on-demand.
|
|
362
|
+
function hasMintPermissionFor(uint256 projectId, JBRuleset memory, address addr) external view returns (bool flag) {
|
|
363
|
+
// If the address is a sucker for this project.
|
|
364
|
+
return SUCKER_REGISTRY.isSuckerOf({projectId: projectId, addr: addr});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/// @dev Make sure only mints can be received.
|
|
368
|
+
function onERC721Received(
|
|
369
|
+
address operator,
|
|
370
|
+
address from,
|
|
371
|
+
uint256 tokenId,
|
|
372
|
+
bytes calldata data
|
|
373
|
+
)
|
|
374
|
+
external
|
|
375
|
+
view
|
|
376
|
+
returns (bytes4)
|
|
377
|
+
{
|
|
378
|
+
data;
|
|
379
|
+
tokenId;
|
|
380
|
+
operator;
|
|
381
|
+
|
|
382
|
+
// Make sure the 721 received is the JBProjects contract.
|
|
383
|
+
if (msg.sender != address(PROJECTS)) revert();
|
|
384
|
+
// Make sure the 721 is being received as a mint.
|
|
385
|
+
if (from != address(0)) revert();
|
|
386
|
+
return IERC721Receiver.onERC721Received.selector;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
//*********************************************************************//
|
|
390
|
+
// -------------------------- public views --------------------------- //
|
|
391
|
+
//*********************************************************************//
|
|
392
|
+
|
|
393
|
+
/// @notice Indicates if this contract adheres to the specified interface.
|
|
394
|
+
/// @dev See `IERC165.supportsInterface`.
|
|
395
|
+
/// @return A flag indicating if the provided interface ID is supported.
|
|
396
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
|
397
|
+
return interfaceId == type(ICTDeployer).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
398
|
+
|| interfaceId == type(IERC721Receiver).interfaceId;
|
|
399
|
+
}
|
|
400
|
+
|
|
392
401
|
//*********************************************************************//
|
|
393
402
|
// --------------------- internal transactions ----------------------- //
|
|
394
403
|
//*********************************************************************//
|
|
@@ -432,7 +441,7 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
432
441
|
}
|
|
433
442
|
|
|
434
443
|
//*********************************************************************//
|
|
435
|
-
//
|
|
444
|
+
// -------------------------- internal views ------------------------- //
|
|
436
445
|
//*********************************************************************//
|
|
437
446
|
|
|
438
447
|
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
package/src/CTPublisher.sol
CHANGED
|
@@ -105,141 +105,6 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
105
105
|
FEE_PROJECT_ID = feeProjectId;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
//*********************************************************************//
|
|
109
|
-
// ------------------------- external views -------------------------- //
|
|
110
|
-
//*********************************************************************//
|
|
111
|
-
|
|
112
|
-
/// @notice Get the tiers for the provided encoded IPFS URIs.
|
|
113
|
-
/// @dev The returned tier IDs may be stale if the corresponding tiers were removed externally via adjustTiers.
|
|
114
|
-
/// In that case, the store's tierOf call will return a tier with default/empty values. Callers should check
|
|
115
|
-
/// the returned tier's initialSupply or other fields to confirm the tier still exists.
|
|
116
|
-
/// @param hook The hook from which to get tiers.
|
|
117
|
-
/// @param encodedIPFSUris The URIs to get tiers of.
|
|
118
|
-
/// @return tiers The tiers that correspond to the provided encoded IPFS URIs. If there's no tier yet, an empty tier
|
|
119
|
-
/// is returned.
|
|
120
|
-
function tiersFor(
|
|
121
|
-
address hook,
|
|
122
|
-
// forge-lint: disable-next-line(mixed-case-variable)
|
|
123
|
-
bytes32[] memory encodedIPFSUris
|
|
124
|
-
)
|
|
125
|
-
external
|
|
126
|
-
view
|
|
127
|
-
override
|
|
128
|
-
returns (JB721Tier[] memory tiers)
|
|
129
|
-
{
|
|
130
|
-
// forge-lint: disable-next-line(mixed-case-variable)
|
|
131
|
-
uint256 numberOfEncodedIPFSUris = encodedIPFSUris.length;
|
|
132
|
-
|
|
133
|
-
// Initialize the tier array being returned.
|
|
134
|
-
tiers = new JB721Tier[](numberOfEncodedIPFSUris);
|
|
135
|
-
|
|
136
|
-
// Get the tier for each provided encoded IPFS URI.
|
|
137
|
-
for (uint256 i; i < numberOfEncodedIPFSUris;) {
|
|
138
|
-
// Check if there's a tier ID stored for the encoded IPFS URI.
|
|
139
|
-
uint256 tierId = tierIdForEncodedIPFSUriOf[hook][encodedIPFSUris[i]];
|
|
140
|
-
|
|
141
|
-
// If there's a tier ID stored, resolve it.
|
|
142
|
-
if (tierId != 0) {
|
|
143
|
-
// slither-disable-next-line calls-loop
|
|
144
|
-
tiers[i] = IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false});
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
unchecked {
|
|
148
|
-
++i;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
//*********************************************************************//
|
|
154
|
-
// -------------------------- public views --------------------------- //
|
|
155
|
-
//*********************************************************************//
|
|
156
|
-
|
|
157
|
-
/// @notice Post allowances for a particular category on a particular hook.
|
|
158
|
-
/// @param hook The hook contract for which this allowance applies.
|
|
159
|
-
/// @param category The category for which this allowance applies.
|
|
160
|
-
/// @return minimumPrice The minimum price that a poster must pay to record a new NFT.
|
|
161
|
-
/// @return minimumTotalSupply The minimum total number of available tokens that a minter must set to record a new
|
|
162
|
-
/// NFT.
|
|
163
|
-
/// @return maximumTotalSupply The max total supply of NFTs that can be made available when minting. Must be >=
|
|
164
|
-
/// minimumTotalSupply.
|
|
165
|
-
/// @return maximumSplitPercent The maximum split percent that a poster can set. 0 means splits are not allowed.
|
|
166
|
-
/// @return allowedAddresses The addresses allowed to post. Returns empty if all addresses are allowed.
|
|
167
|
-
function allowanceFor(
|
|
168
|
-
address hook,
|
|
169
|
-
uint256 category
|
|
170
|
-
)
|
|
171
|
-
public
|
|
172
|
-
view
|
|
173
|
-
override
|
|
174
|
-
returns (
|
|
175
|
-
uint256 minimumPrice,
|
|
176
|
-
uint256 minimumTotalSupply,
|
|
177
|
-
uint256 maximumTotalSupply,
|
|
178
|
-
uint256 maximumSplitPercent,
|
|
179
|
-
address[] memory allowedAddresses
|
|
180
|
-
)
|
|
181
|
-
{
|
|
182
|
-
// Get a reference to the packed values.
|
|
183
|
-
uint256 packed = _packedAllowanceFor[hook][category];
|
|
184
|
-
|
|
185
|
-
// minimum price in bits 0-103 (104 bits).
|
|
186
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
187
|
-
minimumPrice = uint256(uint104(packed));
|
|
188
|
-
// minimum supply in bits 104-135 (32 bits).
|
|
189
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
190
|
-
minimumTotalSupply = uint256(uint32(packed >> 104));
|
|
191
|
-
// maximum supply in bits 136-167 (32 bits).
|
|
192
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
193
|
-
maximumTotalSupply = uint256(uint32(packed >> 136));
|
|
194
|
-
// maximum split percent in bits 168-199 (32 bits).
|
|
195
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
196
|
-
maximumSplitPercent = uint256(uint32(packed >> 168));
|
|
197
|
-
|
|
198
|
-
allowedAddresses = _allowedAddresses[hook][category];
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
//*********************************************************************//
|
|
202
|
-
// -------------------------- internal views ------------------------- //
|
|
203
|
-
//*********************************************************************//
|
|
204
|
-
|
|
205
|
-
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
206
|
-
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
207
|
-
return super._contextSuffixLength();
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/// @notice Check if an address is included in an allow list.
|
|
211
|
-
/// @dev Uses an O(n) linear scan over the `addresses` array. This is acceptable for typical allow list sizes
|
|
212
|
-
/// (fewer than ~100 addresses), where gas cost is negligible. For very large allow lists, a Merkle proof
|
|
213
|
-
/// pattern would scale better, but the added complexity is not warranted for the expected use case.
|
|
214
|
-
/// @param addrs The candidate address.
|
|
215
|
-
/// @param addresses An array of allowed addresses.
|
|
216
|
-
function _isAllowed(address addrs, address[] memory addresses) internal pure returns (bool) {
|
|
217
|
-
// Keep a reference to the number of address to check against.
|
|
218
|
-
uint256 numberOfAddresses = addresses.length;
|
|
219
|
-
|
|
220
|
-
// Check if the address is included
|
|
221
|
-
for (uint256 i; i < numberOfAddresses;) {
|
|
222
|
-
if (addrs == addresses[i]) return true;
|
|
223
|
-
unchecked {
|
|
224
|
-
++i;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/// @notice Returns the calldata, prefered to use over `msg.data`
|
|
232
|
-
/// @return calldata the `msg.data` of this call
|
|
233
|
-
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
234
|
-
return ERC2771Context._msgData();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/// @notice Returns the sender, prefered to use over `msg.sender`
|
|
238
|
-
/// @return sender the sender address of this call.
|
|
239
|
-
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
240
|
-
return ERC2771Context._msgSender();
|
|
241
|
-
}
|
|
242
|
-
|
|
243
108
|
//*********************************************************************//
|
|
244
109
|
// ---------------------- external transactions ---------------------- //
|
|
245
110
|
//*********************************************************************//
|
|
@@ -446,7 +311,100 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
446
311
|
}
|
|
447
312
|
|
|
448
313
|
//*********************************************************************//
|
|
449
|
-
//
|
|
314
|
+
// ------------------------- external views -------------------------- //
|
|
315
|
+
//*********************************************************************//
|
|
316
|
+
|
|
317
|
+
/// @notice Get the tiers for the provided encoded IPFS URIs.
|
|
318
|
+
/// @dev The returned tier IDs may be stale if the corresponding tiers were removed externally via adjustTiers.
|
|
319
|
+
/// In that case, the store's tierOf call will return a tier with default/empty values. Callers should check
|
|
320
|
+
/// the returned tier's initialSupply or other fields to confirm the tier still exists.
|
|
321
|
+
/// @param hook The hook from which to get tiers.
|
|
322
|
+
/// @param encodedIPFSUris The URIs to get tiers of.
|
|
323
|
+
/// @return tiers The tiers that correspond to the provided encoded IPFS URIs. If there's no tier yet, an empty tier
|
|
324
|
+
/// is returned.
|
|
325
|
+
function tiersFor(
|
|
326
|
+
address hook,
|
|
327
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
328
|
+
bytes32[] memory encodedIPFSUris
|
|
329
|
+
)
|
|
330
|
+
external
|
|
331
|
+
view
|
|
332
|
+
override
|
|
333
|
+
returns (JB721Tier[] memory tiers)
|
|
334
|
+
{
|
|
335
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
336
|
+
uint256 numberOfEncodedIPFSUris = encodedIPFSUris.length;
|
|
337
|
+
|
|
338
|
+
// Initialize the tier array being returned.
|
|
339
|
+
tiers = new JB721Tier[](numberOfEncodedIPFSUris);
|
|
340
|
+
|
|
341
|
+
// Get the tier for each provided encoded IPFS URI.
|
|
342
|
+
for (uint256 i; i < numberOfEncodedIPFSUris;) {
|
|
343
|
+
// Check if there's a tier ID stored for the encoded IPFS URI.
|
|
344
|
+
uint256 tierId = tierIdForEncodedIPFSUriOf[hook][encodedIPFSUris[i]];
|
|
345
|
+
|
|
346
|
+
// If there's a tier ID stored, resolve it.
|
|
347
|
+
if (tierId != 0) {
|
|
348
|
+
// slither-disable-next-line calls-loop
|
|
349
|
+
tiers[i] = IJB721TiersHook(hook).STORE().tierOf({hook: hook, id: tierId, includeResolvedUri: false});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
unchecked {
|
|
353
|
+
++i;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
//*********************************************************************//
|
|
359
|
+
// -------------------------- public views --------------------------- //
|
|
360
|
+
//*********************************************************************//
|
|
361
|
+
|
|
362
|
+
/// @notice Post allowances for a particular category on a particular hook.
|
|
363
|
+
/// @param hook The hook contract for which this allowance applies.
|
|
364
|
+
/// @param category The category for which this allowance applies.
|
|
365
|
+
/// @return minimumPrice The minimum price that a poster must pay to record a new NFT.
|
|
366
|
+
/// @return minimumTotalSupply The minimum total number of available tokens that a minter must set to record a new
|
|
367
|
+
/// NFT.
|
|
368
|
+
/// @return maximumTotalSupply The max total supply of NFTs that can be made available when minting. Must be >=
|
|
369
|
+
/// minimumTotalSupply.
|
|
370
|
+
/// @return maximumSplitPercent The maximum split percent that a poster can set. 0 means splits are not allowed.
|
|
371
|
+
/// @return allowedAddresses The addresses allowed to post. Returns empty if all addresses are allowed.
|
|
372
|
+
function allowanceFor(
|
|
373
|
+
address hook,
|
|
374
|
+
uint256 category
|
|
375
|
+
)
|
|
376
|
+
public
|
|
377
|
+
view
|
|
378
|
+
override
|
|
379
|
+
returns (
|
|
380
|
+
uint256 minimumPrice,
|
|
381
|
+
uint256 minimumTotalSupply,
|
|
382
|
+
uint256 maximumTotalSupply,
|
|
383
|
+
uint256 maximumSplitPercent,
|
|
384
|
+
address[] memory allowedAddresses
|
|
385
|
+
)
|
|
386
|
+
{
|
|
387
|
+
// Get a reference to the packed values.
|
|
388
|
+
uint256 packed = _packedAllowanceFor[hook][category];
|
|
389
|
+
|
|
390
|
+
// minimum price in bits 0-103 (104 bits).
|
|
391
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
392
|
+
minimumPrice = uint256(uint104(packed));
|
|
393
|
+
// minimum supply in bits 104-135 (32 bits).
|
|
394
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
395
|
+
minimumTotalSupply = uint256(uint32(packed >> 104));
|
|
396
|
+
// maximum supply in bits 136-167 (32 bits).
|
|
397
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
398
|
+
maximumTotalSupply = uint256(uint32(packed >> 136));
|
|
399
|
+
// maximum split percent in bits 168-199 (32 bits).
|
|
400
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
401
|
+
maximumSplitPercent = uint256(uint32(packed >> 168));
|
|
402
|
+
|
|
403
|
+
allowedAddresses = _allowedAddresses[hook][category];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
//*********************************************************************//
|
|
407
|
+
// ------------------------ internal helpers ------------------------- //
|
|
450
408
|
//*********************************************************************//
|
|
451
409
|
|
|
452
410
|
/// @notice Setup the posts.
|
|
@@ -613,4 +571,46 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
613
571
|
}
|
|
614
572
|
}
|
|
615
573
|
}
|
|
574
|
+
|
|
575
|
+
/// @notice Check if an address is included in an allow list.
|
|
576
|
+
/// @dev Uses an O(n) linear scan over the `addresses` array. This is acceptable for typical allow list sizes
|
|
577
|
+
/// (fewer than ~100 addresses), where gas cost is negligible. For very large allow lists, a Merkle proof
|
|
578
|
+
/// pattern would scale better, but the added complexity is not warranted for the expected use case.
|
|
579
|
+
/// @param addrs The candidate address.
|
|
580
|
+
/// @param addresses An array of allowed addresses.
|
|
581
|
+
function _isAllowed(address addrs, address[] memory addresses) internal pure returns (bool) {
|
|
582
|
+
// Keep a reference to the number of address to check against.
|
|
583
|
+
uint256 numberOfAddresses = addresses.length;
|
|
584
|
+
|
|
585
|
+
// Check if the address is included
|
|
586
|
+
for (uint256 i; i < numberOfAddresses;) {
|
|
587
|
+
if (addrs == addresses[i]) return true;
|
|
588
|
+
unchecked {
|
|
589
|
+
++i;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
//*********************************************************************//
|
|
597
|
+
// -------------------------- internal views ------------------------- //
|
|
598
|
+
//*********************************************************************//
|
|
599
|
+
|
|
600
|
+
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
601
|
+
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
602
|
+
return super._contextSuffixLength();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/// @notice Returns the calldata, prefered to use over `msg.data`
|
|
606
|
+
/// @return calldata the `msg.data` of this call
|
|
607
|
+
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
608
|
+
return ERC2771Context._msgData();
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/// @notice Returns the sender, prefered to use over `msg.sender`
|
|
612
|
+
/// @return sender the sender address of this call.
|
|
613
|
+
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
614
|
+
return ERC2771Context._msgSender();
|
|
615
|
+
}
|
|
616
616
|
}
|
package/test/CTDeployer.t.sol
CHANGED
|
@@ -56,10 +56,11 @@ contract MockDataHook is IJBRulesetDataHook {
|
|
|
56
56
|
uint256 cashOutTaxRate,
|
|
57
57
|
uint256 cashOutCount,
|
|
58
58
|
uint256 totalSupply,
|
|
59
|
+
uint256 surplusValue,
|
|
59
60
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
60
61
|
)
|
|
61
62
|
{
|
|
62
|
-
return (TAX_RATE, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
63
|
+
return (TAX_RATE, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function beforePayRecordedWith(JBBeforePayRecordedContext calldata)
|
|
@@ -434,7 +435,7 @@ contract TestCTDeployer is Test {
|
|
|
434
435
|
JBBeforeCashOutRecordedContext memory context =
|
|
435
436
|
_buildCashOutContext(deployedProjectId, unauthorized, 100e18, 1000e18);
|
|
436
437
|
|
|
437
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
438
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
438
439
|
|
|
439
440
|
assertEq(taxRate, 5000, "tax rate should come from data hook");
|
|
440
441
|
assertEq(cashOutCount, 100e18, "cashOutCount should be forwarded");
|
|
@@ -455,7 +456,7 @@ contract TestCTDeployer is Test {
|
|
|
455
456
|
JBBeforeCashOutRecordedContext memory context =
|
|
456
457
|
_buildCashOutContext(deployedProjectId, suckerAddr, 100e18, 1000e18);
|
|
457
458
|
|
|
458
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
459
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
459
460
|
|
|
460
461
|
assertEq(taxRate, 0, "sucker should get 0% tax rate");
|
|
461
462
|
assertEq(cashOutCount, 100e18, "cashOutCount should pass through");
|
|
@@ -466,7 +467,7 @@ contract TestCTDeployer is Test {
|
|
|
466
467
|
function test_beforeCashOutRecordedWith_returnsDefaultsWhenNoDataHook() public {
|
|
467
468
|
JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(999, unauthorized, 100e18, 1000e18);
|
|
468
469
|
|
|
469
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
470
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,, JBCashOutHookSpecification[] memory specs) =
|
|
470
471
|
deployer.beforeCashOutRecordedWith(context);
|
|
471
472
|
|
|
472
473
|
assertEq(taxRate, context.cashOutTaxRate, "cashOutTaxRate should be returned as-is from context");
|
package/test/Fork.t.sol
CHANGED
|
@@ -21,6 +21,7 @@ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
|
21
21
|
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
22
22
|
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
23
23
|
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
24
|
+
import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
|
|
24
25
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
25
26
|
|
|
26
27
|
// Suckers — deploy fresh within fork.
|
|
@@ -166,7 +167,7 @@ contract ForkTest is Test {
|
|
|
166
167
|
jbPermissions = new JBPermissions(trustedForwarder);
|
|
167
168
|
jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
|
|
168
169
|
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
169
|
-
JBERC20 jbErc20 = new JBERC20();
|
|
170
|
+
JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
|
|
170
171
|
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
171
172
|
jbRulesets = new JBRulesets(jbDirectory);
|
|
172
173
|
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
|
|
@@ -193,9 +194,11 @@ contract ForkTest is Test {
|
|
|
193
194
|
function _deploy721Hook() internal {
|
|
194
195
|
JB721TiersHookStore store = new JB721TiersHookStore();
|
|
195
196
|
JBAddressRegistry addressRegistry = new JBAddressRegistry();
|
|
197
|
+
JB721CheckpointsDeployer checkpointsDeployer = new JB721CheckpointsDeployer();
|
|
196
198
|
|
|
197
|
-
JB721TiersHook hookImpl =
|
|
198
|
-
|
|
199
|
+
JB721TiersHook hookImpl = new JB721TiersHook(
|
|
200
|
+
jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, checkpointsDeployer, trustedForwarder
|
|
201
|
+
);
|
|
199
202
|
|
|
200
203
|
hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
|
|
201
204
|
}
|
package/test/TestAuditGaps.sol
CHANGED
|
@@ -37,7 +37,7 @@ contract RevertingDataHook is IJBRulesetDataHook {
|
|
|
37
37
|
external
|
|
38
38
|
pure
|
|
39
39
|
override
|
|
40
|
-
returns (uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
40
|
+
returns (uint256, uint256, uint256, uint256, JBCashOutHookSpecification[] memory)
|
|
41
41
|
{
|
|
42
42
|
revert("DATA_HOOK_REVERTED");
|
|
43
43
|
}
|
|
@@ -81,10 +81,11 @@ contract SuccessDataHook is IJBRulesetDataHook {
|
|
|
81
81
|
uint256 cashOutTaxRate,
|
|
82
82
|
uint256 cashOutCount,
|
|
83
83
|
uint256 totalSupply,
|
|
84
|
+
uint256 surplusValue,
|
|
84
85
|
JBCashOutHookSpecification[] memory hookSpecifications
|
|
85
86
|
)
|
|
86
87
|
{
|
|
87
|
-
return (TAX_RATE, context.cashOutCount, context.totalSupply, hookSpecifications);
|
|
88
|
+
return (TAX_RATE, context.cashOutCount, context.totalSupply, context.surplus.value, hookSpecifications);
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
@@ -243,7 +244,7 @@ contract TestAuditGaps is Test {
|
|
|
243
244
|
// dataHookOf[999] is address(0) by default (never set).
|
|
244
245
|
JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(999, unauthorized, 100e18, 1000e18);
|
|
245
246
|
|
|
246
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
247
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,, JBCashOutHookSpecification[] memory specs) =
|
|
247
248
|
deployer.beforeCashOutRecordedWith(context);
|
|
248
249
|
|
|
249
250
|
assertEq(taxRate, context.cashOutTaxRate, "cashOutTaxRate should be returned as-is from context");
|
|
@@ -269,7 +270,7 @@ contract TestAuditGaps is Test {
|
|
|
269
270
|
JBBeforeCashOutRecordedContext memory context =
|
|
270
271
|
_buildCashOutContext(hookProjectId, unauthorized, 100e18, 1000e18);
|
|
271
272
|
|
|
272
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
273
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
273
274
|
|
|
274
275
|
assertEq(taxRate, 5000, "tax rate should be forwarded from data hook");
|
|
275
276
|
assertEq(cashOutCount, 100e18, "cashOutCount should be forwarded");
|
|
@@ -302,7 +303,7 @@ contract TestAuditGaps is Test {
|
|
|
302
303
|
JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, realSucker, 100e18, 1000e18);
|
|
303
304
|
|
|
304
305
|
// Should NOT revert because suckers bypass the data hook entirely.
|
|
305
|
-
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply
|
|
306
|
+
(uint256 taxRate, uint256 cashOutCount, uint256 totalSupply,,) = deployer.beforeCashOutRecordedWith(context);
|
|
306
307
|
|
|
307
308
|
assertEq(taxRate, 0, "sucker should get 0% tax rate");
|
|
308
309
|
assertEq(cashOutCount, 100e18, "cashOutCount should pass through");
|
|
@@ -320,7 +321,7 @@ contract TestAuditGaps is Test {
|
|
|
320
321
|
// fakeSucker is NOT registered as a sucker (default mock returns false).
|
|
321
322
|
JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, fakeSucker, 100e18, 1000e18);
|
|
322
323
|
|
|
323
|
-
(uint256 taxRate
|
|
324
|
+
(uint256 taxRate,,,,) = deployer.beforeCashOutRecordedWith(context);
|
|
324
325
|
|
|
325
326
|
// Should get the data hook's tax rate (5000), not 0.
|
|
326
327
|
assertEq(taxRate, 5000, "non-sucker should not bypass tax");
|
|
@@ -340,7 +341,7 @@ contract TestAuditGaps is Test {
|
|
|
340
341
|
|
|
341
342
|
JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, realSucker, 100e18, 1000e18);
|
|
342
343
|
|
|
343
|
-
(uint256 taxRate
|
|
344
|
+
(uint256 taxRate,,,,) = deployer.beforeCashOutRecordedWith(context);
|
|
344
345
|
|
|
345
346
|
// Should get the data hook's tax rate, not 0.
|
|
346
347
|
assertEq(taxRate, 5000, "sucker from wrong project should not bypass tax");
|
|
@@ -359,7 +360,7 @@ contract TestAuditGaps is Test {
|
|
|
359
360
|
|
|
360
361
|
JBBeforeCashOutRecordedContext memory context = _buildCashOutContext(hookProjectId, realSucker, 100e18, 1000e18);
|
|
361
362
|
|
|
362
|
-
(uint256 taxRate
|
|
363
|
+
(uint256 taxRate,,,,) = deployer.beforeCashOutRecordedWith(context);
|
|
363
364
|
|
|
364
365
|
assertEq(taxRate, 0, "valid sucker should get 0% tax");
|
|
365
366
|
}
|
|
@@ -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 {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
9
|
+
import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
|
|
10
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
11
|
+
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
12
|
+
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
13
|
+
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
14
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
15
|
+
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
16
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
17
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
18
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
19
|
+
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
20
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
21
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
22
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
23
|
+
|
|
24
|
+
import {CTDeployer} from "../../src/CTDeployer.sol";
|
|
25
|
+
import {CTPublisher} from "../../src/CTPublisher.sol";
|
|
26
|
+
import {ICTPublisher} from "../../src/interfaces/ICTPublisher.sol";
|
|
27
|
+
import {CTDeployerAllowedPost} from "../../src/structs/CTDeployerAllowedPost.sol";
|
|
28
|
+
import {CTProjectConfig} from "../../src/structs/CTProjectConfig.sol";
|
|
29
|
+
import {CTSuckerDeploymentConfig} from "../../src/structs/CTSuckerDeploymentConfig.sol";
|
|
30
|
+
|
|
31
|
+
contract NemesisMockProjects {
|
|
32
|
+
uint256 public countValue;
|
|
33
|
+
mapping(uint256 => address) internal _ownerOf;
|
|
34
|
+
|
|
35
|
+
function setCount(uint256 count_) external {
|
|
36
|
+
countValue = count_;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setOwner(uint256 projectId, address owner_) external {
|
|
40
|
+
_ownerOf[projectId] = owner_;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function count() external view returns (uint256) {
|
|
44
|
+
return countValue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function ownerOf(uint256 projectId) external view returns (address) {
|
|
48
|
+
return _ownerOf[projectId];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function transferFrom(address from, address to, uint256 tokenId) external {
|
|
52
|
+
require(_ownerOf[tokenId] == from, "wrong from");
|
|
53
|
+
_ownerOf[tokenId] = to;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
contract NemesisMockController {
|
|
58
|
+
NemesisMockProjects public immutable PROJECTS;
|
|
59
|
+
uint256 public immutable nextProjectId;
|
|
60
|
+
|
|
61
|
+
constructor(NemesisMockProjects projects_, uint256 nextProjectId_) {
|
|
62
|
+
PROJECTS = projects_;
|
|
63
|
+
nextProjectId = nextProjectId_;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function launchProjectFor(
|
|
67
|
+
address owner,
|
|
68
|
+
string calldata,
|
|
69
|
+
JBRulesetConfig[] calldata,
|
|
70
|
+
JBTerminalConfig[] calldata,
|
|
71
|
+
string calldata
|
|
72
|
+
)
|
|
73
|
+
external
|
|
74
|
+
returns (uint256)
|
|
75
|
+
{
|
|
76
|
+
PROJECTS.setOwner(nextProjectId, owner);
|
|
77
|
+
return nextProjectId;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
contract NemesisPermissionedHook is JBPermissioned {
|
|
82
|
+
address public immutable ownerAccount;
|
|
83
|
+
uint256 public immutable projectId;
|
|
84
|
+
bool public adjusted;
|
|
85
|
+
|
|
86
|
+
constructor(IJBPermissions permissions, address ownerAccount_, uint256 projectId_) JBPermissioned(permissions) {
|
|
87
|
+
ownerAccount = ownerAccount_;
|
|
88
|
+
projectId = projectId_;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
92
|
+
function PROJECT_ID() external view returns (uint256) {
|
|
93
|
+
return projectId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function owner() external view returns (address) {
|
|
97
|
+
return ownerAccount;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function adjustTiers(JB721TierConfig[] calldata, uint256[] calldata) external {
|
|
101
|
+
_requirePermissionFrom(ownerAccount, projectId, JBPermissionIds.ADJUST_721_TIERS);
|
|
102
|
+
adjusted = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
contract NemesisMockHookDeployer {
|
|
107
|
+
IJB721TiersHook public hook;
|
|
108
|
+
|
|
109
|
+
function setHook(IJB721TiersHook hook_) external {
|
|
110
|
+
hook = hook_;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function deployHookFor(
|
|
114
|
+
uint256,
|
|
115
|
+
JBDeploy721TiersHookConfig calldata,
|
|
116
|
+
bytes32
|
|
117
|
+
)
|
|
118
|
+
external
|
|
119
|
+
view
|
|
120
|
+
returns (IJB721TiersHook)
|
|
121
|
+
{
|
|
122
|
+
return hook;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
contract NemesisNoopSuckerRegistry {
|
|
127
|
+
function isSuckerOf(uint256, address) external pure returns (bool) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function deploySuckersFor(
|
|
132
|
+
uint256,
|
|
133
|
+
bytes32,
|
|
134
|
+
JBSuckerDeployerConfig[] calldata
|
|
135
|
+
)
|
|
136
|
+
external
|
|
137
|
+
pure
|
|
138
|
+
returns (address[] memory suckers)
|
|
139
|
+
{
|
|
140
|
+
return suckers;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
contract NemesisMockDirectory {
|
|
145
|
+
IJBProjects public immutable PROJECTS;
|
|
146
|
+
|
|
147
|
+
constructor(IJBProjects projects_) {
|
|
148
|
+
PROJECTS = projects_;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
contract CodexNemesisPoCs is Test {
|
|
153
|
+
JBPermissions permissions;
|
|
154
|
+
NemesisMockProjects projects;
|
|
155
|
+
NemesisMockHookDeployer hookDeployer;
|
|
156
|
+
NemesisMockController controller;
|
|
157
|
+
CTPublisher publisher;
|
|
158
|
+
CTDeployer deployer;
|
|
159
|
+
NemesisPermissionedHook hook;
|
|
160
|
+
|
|
161
|
+
address alice = makeAddr("alice");
|
|
162
|
+
address bob = makeAddr("bob");
|
|
163
|
+
|
|
164
|
+
function setUp() public {
|
|
165
|
+
permissions = new JBPermissions(address(0));
|
|
166
|
+
projects = new NemesisMockProjects();
|
|
167
|
+
projects.setCount(5);
|
|
168
|
+
|
|
169
|
+
hookDeployer = new NemesisMockHookDeployer();
|
|
170
|
+
publisher = new CTPublisher(IJBDirectory(makeAddr("directory")), permissions, 1, address(0));
|
|
171
|
+
deployer = new CTDeployer(
|
|
172
|
+
permissions,
|
|
173
|
+
IJBProjects(address(projects)),
|
|
174
|
+
IJB721TiersHookDeployer(address(hookDeployer)),
|
|
175
|
+
ICTPublisher(address(publisher)),
|
|
176
|
+
IJBSuckerRegistry(address(new NemesisNoopSuckerRegistry())),
|
|
177
|
+
address(0)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
hook = new NemesisPermissionedHook(permissions, address(deployer), 6);
|
|
181
|
+
hookDeployer.setHook(IJB721TiersHook(address(hook)));
|
|
182
|
+
controller = new NemesisMockController(projects, 6);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function test_oldProjectOwnerRetainsHookControlAfterProjectNftTransferUntilClaim() public {
|
|
186
|
+
CTProjectConfig memory config = CTProjectConfig({
|
|
187
|
+
terminalConfigurations: new JBTerminalConfig[](0),
|
|
188
|
+
projectUri: "ipfs://project",
|
|
189
|
+
allowedPosts: new CTDeployerAllowedPost[](0),
|
|
190
|
+
contractUri: "ipfs://contract",
|
|
191
|
+
name: "Croptop",
|
|
192
|
+
symbol: "CT",
|
|
193
|
+
salt: bytes32(0)
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
CTSuckerDeploymentConfig memory suckerConfig =
|
|
197
|
+
CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
|
|
198
|
+
|
|
199
|
+
deployer.deployProjectFor(alice, config, suckerConfig, IJBController(address(controller)));
|
|
200
|
+
assertEq(projects.ownerOf(6), alice, "alice should receive the project NFT");
|
|
201
|
+
|
|
202
|
+
projects.transferFrom(alice, bob, 6);
|
|
203
|
+
assertEq(projects.ownerOf(6), bob, "bob should own the project NFT after transfer");
|
|
204
|
+
|
|
205
|
+
JB721TierConfig[] memory arbitraryTiers = new JB721TierConfig[](0);
|
|
206
|
+
uint256[] memory removals = new uint256[](0);
|
|
207
|
+
|
|
208
|
+
vm.prank(alice);
|
|
209
|
+
NemesisPermissionedHook(address(hook)).adjustTiers(arbitraryTiers, removals);
|
|
210
|
+
|
|
211
|
+
assertTrue(
|
|
212
|
+
hook.adjusted(),
|
|
213
|
+
"the previous owner should still be able to mutate hook state until the new NFT owner explicitly claims"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function test_deploySuckersHelperBreaksAfterOwnershipTransferBecauseRegistrySeesCtDeployerAsCaller() public {
|
|
218
|
+
NemesisMockDirectory directory = new NemesisMockDirectory(IJBProjects(address(projects)));
|
|
219
|
+
JBSuckerRegistry registry =
|
|
220
|
+
new JBSuckerRegistry(IJBDirectory(address(directory)), permissions, address(this), address(0));
|
|
221
|
+
|
|
222
|
+
deployer = new CTDeployer(
|
|
223
|
+
permissions,
|
|
224
|
+
IJBProjects(address(projects)),
|
|
225
|
+
IJB721TiersHookDeployer(address(hookDeployer)),
|
|
226
|
+
ICTPublisher(address(publisher)),
|
|
227
|
+
IJBSuckerRegistry(address(registry)),
|
|
228
|
+
address(0)
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
hook = new NemesisPermissionedHook(permissions, address(deployer), 6);
|
|
232
|
+
hookDeployer.setHook(IJB721TiersHook(address(hook)));
|
|
233
|
+
|
|
234
|
+
CTProjectConfig memory config = CTProjectConfig({
|
|
235
|
+
terminalConfigurations: new JBTerminalConfig[](0),
|
|
236
|
+
projectUri: "ipfs://project",
|
|
237
|
+
allowedPosts: new CTDeployerAllowedPost[](0),
|
|
238
|
+
contractUri: "ipfs://contract",
|
|
239
|
+
name: "Croptop",
|
|
240
|
+
symbol: "CT",
|
|
241
|
+
salt: bytes32(0)
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
CTSuckerDeploymentConfig memory emptySuckerConfig =
|
|
245
|
+
CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
|
|
246
|
+
deployer.deployProjectFor(alice, config, emptySuckerConfig, IJBController(address(controller)));
|
|
247
|
+
|
|
248
|
+
CTSuckerDeploymentConfig memory laterSuckerConfig =
|
|
249
|
+
CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32("later")});
|
|
250
|
+
|
|
251
|
+
vm.prank(alice);
|
|
252
|
+
vm.expectRevert(
|
|
253
|
+
abi.encodeWithSelector(
|
|
254
|
+
JBPermissioned.JBPermissioned_Unauthorized.selector,
|
|
255
|
+
alice,
|
|
256
|
+
address(deployer),
|
|
257
|
+
6,
|
|
258
|
+
JBPermissionIds.DEPLOY_SUCKERS
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
deployer.deploySuckersFor(6, laterSuckerConfig);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -32,6 +32,7 @@ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
|
32
32
|
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
33
33
|
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
34
34
|
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
35
|
+
import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
|
|
35
36
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
36
37
|
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
37
38
|
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
@@ -276,7 +277,7 @@ contract PublishForkTest is Test, DeployPermit2 {
|
|
|
276
277
|
jbPermissions = new JBPermissions(trustedForwarder);
|
|
277
278
|
jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
|
|
278
279
|
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
279
|
-
JBERC20 jbErc20 = new JBERC20();
|
|
280
|
+
JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
|
|
280
281
|
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
281
282
|
jbRulesets = new JBRulesets(jbDirectory);
|
|
282
283
|
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
|
|
@@ -321,9 +322,11 @@ contract PublishForkTest is Test, DeployPermit2 {
|
|
|
321
322
|
function _deploy721Hook() internal {
|
|
322
323
|
JB721TiersHookStore store = new JB721TiersHookStore();
|
|
323
324
|
JBAddressRegistry addressRegistry = new JBAddressRegistry();
|
|
325
|
+
JB721CheckpointsDeployer checkpointsDeployer = new JB721CheckpointsDeployer();
|
|
324
326
|
|
|
325
|
-
JB721TiersHook hookImpl =
|
|
326
|
-
|
|
327
|
+
JB721TiersHook hookImpl = new JB721TiersHook(
|
|
328
|
+
jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, checkpointsDeployer, trustedForwarder
|
|
329
|
+
);
|
|
327
330
|
|
|
328
331
|
hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
|
|
329
332
|
}
|