@croptop/core-v6 0.0.8 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ADMINISTRATION.md +133 -0
- package/ARCHITECTURE.md +66 -0
- package/RISKS.md +238 -0
- package/STYLE_GUIDE.md +481 -0
- package/foundry.toml +2 -5
- package/package.json +9 -9
- package/remappings.txt +1 -0
- package/script/ConfigureFeeProject.s.sol +7 -4
- package/src/CTDeployer.sol +56 -56
- package/src/CTProjectOwner.sol +1 -1
- package/src/CTPublisher.sol +11 -3
- package/src/interfaces/ICTDeployer.sol +11 -11
- package/src/interfaces/ICTProjectOwner.sol +0 -1
- package/src/interfaces/ICTPublisher.sol +33 -19
- package/test/Fork.t.sol +152 -39
- package/test/regression/H19_FeeEvasion.t.sol +1 -1
- package/test/regression/L52_StaleTierIdMapping.t.sol +2 -2
- package/test/regression/M6_DuplicateUriFeeEvasion.t.sol +312 -0
package/src/CTDeployer.sol
CHANGED
|
@@ -1,21 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
7
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
8
|
-
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
9
|
-
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
10
|
-
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
11
|
-
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
12
|
-
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
13
|
-
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
14
|
-
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
15
|
-
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
16
|
-
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
4
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
5
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
17
6
|
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
18
|
-
|
|
19
7
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
20
8
|
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
21
9
|
import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
|
|
@@ -24,20 +12,31 @@ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfi
|
|
|
24
12
|
import {JB721TiersHookFlags} from "@bananapus/721-hook-v6/src/structs/JB721TiersHookFlags.sol";
|
|
25
13
|
import {JBDeploy721TiersHookConfig} from "@bananapus/721-hook-v6/src/structs/JBDeploy721TiersHookConfig.sol";
|
|
26
14
|
import {JBLaunchProjectConfig} from "@bananapus/721-hook-v6/src/structs/JBLaunchProjectConfig.sol";
|
|
15
|
+
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
27
16
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
17
|
+
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
28
18
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
19
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
20
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
29
21
|
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
22
|
+
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
23
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
24
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
25
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
26
|
+
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
27
|
+
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
28
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
30
29
|
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
31
|
-
import {
|
|
32
|
-
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
30
|
+
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
33
31
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
32
|
+
import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
|
|
34
33
|
|
|
35
34
|
import {ICTDeployer} from "./interfaces/ICTDeployer.sol";
|
|
36
35
|
import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
|
|
37
36
|
import {CTAllowedPost} from "./structs/CTAllowedPost.sol";
|
|
38
|
-
import {CTSuckerDeploymentConfig} from "./structs/CTSuckerDeploymentConfig.sol";
|
|
39
37
|
import {CTDeployerAllowedPost} from "./structs/CTDeployerAllowedPost.sol";
|
|
40
38
|
import {CTProjectConfig} from "./structs/CTProjectConfig.sol";
|
|
39
|
+
import {CTSuckerDeploymentConfig} from "./structs/CTSuckerDeploymentConfig.sol";
|
|
41
40
|
|
|
42
41
|
/// @notice A contract that facilitates deploying a simple Juicebox project to receive posts from Croptop templates.
|
|
43
42
|
contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC721Receiver, ICTDeployer {
|
|
@@ -124,24 +123,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
124
123
|
// ------------------------- external views -------------------------- //
|
|
125
124
|
//*********************************************************************//
|
|
126
125
|
|
|
127
|
-
/// @notice Forward the call to the original data hook.
|
|
128
|
-
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
|
|
129
|
-
/// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
|
|
130
|
-
/// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
|
|
131
|
-
/// tokens get minted by a payment.
|
|
132
|
-
/// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
|
|
133
|
-
/// into the project. Useful for automatically routing funds from a treasury as payments come in.
|
|
134
|
-
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
135
|
-
external
|
|
136
|
-
view
|
|
137
|
-
override
|
|
138
|
-
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
139
|
-
{
|
|
140
|
-
// Forward the call to the data hook.
|
|
141
|
-
// slither-disable-next-line unused-return
|
|
142
|
-
return dataHookOf[context.projectId].beforePayRecordedWith(context);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
126
|
/// @notice Allow cash outs from suckers without a tax.
|
|
146
127
|
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a cash out.
|
|
147
128
|
/// @param context Standard Juicebox cash out context. See `JBBeforeCashOutRecordedContext`.
|
|
@@ -171,6 +152,24 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
171
152
|
return dataHookOf[context.projectId].beforeCashOutRecordedWith(context);
|
|
172
153
|
}
|
|
173
154
|
|
|
155
|
+
/// @notice Forward the call to the original data hook.
|
|
156
|
+
/// @dev This function is part of `IJBRulesetDataHook`, and gets called before the revnet processes a payment.
|
|
157
|
+
/// @param context Standard Juicebox payment context. See `JBBeforePayRecordedContext`.
|
|
158
|
+
/// @return weight The weight which project tokens are minted relative to. This can be used to customize how many
|
|
159
|
+
/// tokens get minted by a payment.
|
|
160
|
+
/// @return hookSpecifications Amounts (out of what's being paid in) to be sent to pay hooks instead of being paid
|
|
161
|
+
/// into the project. Useful for automatically routing funds from a treasury as payments come in.
|
|
162
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
163
|
+
external
|
|
164
|
+
view
|
|
165
|
+
override
|
|
166
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
167
|
+
{
|
|
168
|
+
// Forward the call to the data hook.
|
|
169
|
+
// slither-disable-next-line unused-return
|
|
170
|
+
return dataHookOf[context.projectId].beforePayRecordedWith(context);
|
|
171
|
+
}
|
|
172
|
+
|
|
174
173
|
/// @notice A flag indicating whether an address has permission to mint a project's tokens on-demand.
|
|
175
174
|
/// @dev A project's data hook can allow any address to mint its tokens.
|
|
176
175
|
/// @param projectId The ID of the project whose token can be minted.
|
|
@@ -219,6 +218,21 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
219
218
|
// ---------------------- external transactions ---------------------- //
|
|
220
219
|
//*********************************************************************//
|
|
221
220
|
|
|
221
|
+
/// @notice Claim ownership of the collection.
|
|
222
|
+
/// @param hook The hook to claim ownership of.
|
|
223
|
+
function claimCollectionOwnershipOf(IJB721TiersHook hook) external override {
|
|
224
|
+
// Get the project ID of the hook.
|
|
225
|
+
uint256 projectId = hook.PROJECT_ID();
|
|
226
|
+
|
|
227
|
+
// Make sure the caller is the owner of the project.
|
|
228
|
+
if (PROJECTS.ownerOf(projectId) != _msgSender()) {
|
|
229
|
+
revert CTDeployer_NotOwnerOfProject(projectId, address(hook), _msgSender());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Transfer the hook's ownership to the project.
|
|
233
|
+
JBOwnable(address(hook)).transferOwnershipToProject(projectId);
|
|
234
|
+
}
|
|
235
|
+
|
|
222
236
|
/// @notice Deploy a simple project meant to receive posts from Croptop templates.
|
|
223
237
|
/// @param owner The address that'll own the project.
|
|
224
238
|
/// @param projectConfig The configuration for the project.
|
|
@@ -266,7 +280,8 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
266
280
|
noNewTiersWithReserves: false,
|
|
267
281
|
noNewTiersWithVotes: false,
|
|
268
282
|
noNewTiersWithOwnerMinting: false,
|
|
269
|
-
preventOverspending: false
|
|
283
|
+
preventOverspending: false,
|
|
284
|
+
issueTokensForSplits: false
|
|
270
285
|
})
|
|
271
286
|
}),
|
|
272
287
|
salt: keccak256(abi.encode(projectConfig.salt, _msgSender()))
|
|
@@ -325,21 +340,6 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
325
340
|
});
|
|
326
341
|
}
|
|
327
342
|
|
|
328
|
-
/// @notice Claim ownership of the collection.
|
|
329
|
-
/// @param hook The hook to claim ownership of.
|
|
330
|
-
function claimCollectionOwnershipOf(IJB721TiersHook hook) external override {
|
|
331
|
-
// Get the project ID of the hook.
|
|
332
|
-
uint256 projectId = hook.PROJECT_ID();
|
|
333
|
-
|
|
334
|
-
// Make sure the caller is the owner of the project.
|
|
335
|
-
if (PROJECTS.ownerOf(projectId) != _msgSender()) {
|
|
336
|
-
revert CTDeployer_NotOwnerOfProject(projectId, address(hook), _msgSender());
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Transfer the hook's ownership to the project.
|
|
340
|
-
JBOwnable(address(hook)).transferOwnershipToProject(projectId);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
343
|
/// @notice Deploy new suckers for an existing project.
|
|
344
344
|
/// @dev Only the juicebox's owner can deploy new suckers.
|
|
345
345
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
@@ -407,6 +407,11 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
407
407
|
// ------------------------ internal functions ----------------------- //
|
|
408
408
|
//*********************************************************************//
|
|
409
409
|
|
|
410
|
+
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
411
|
+
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
412
|
+
return ERC2771Context._contextSuffixLength();
|
|
413
|
+
}
|
|
414
|
+
|
|
410
415
|
/// @notice The calldata. Preferred to use over `msg.data`.
|
|
411
416
|
/// @return calldata The `msg.data` of this call.
|
|
412
417
|
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
@@ -418,9 +423,4 @@ contract CTDeployer is ERC2771Context, JBPermissioned, IJBRulesetDataHook, IERC7
|
|
|
418
423
|
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
419
424
|
return ERC2771Context._msgSender();
|
|
420
425
|
}
|
|
421
|
-
|
|
422
|
-
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
423
|
-
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
424
|
-
return ERC2771Context._contextSuffixLength();
|
|
425
|
-
}
|
|
426
426
|
}
|
package/src/CTProjectOwner.sol
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
4
5
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
5
6
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
6
7
|
import {JBPermissionsData} from "@bananapus/core-v6/src/structs/JBPermissionsData.sol";
|
|
7
8
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
8
|
-
import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
|
|
9
9
|
|
|
10
10
|
import {ICTProjectOwner} from "./interfaces/ICTProjectOwner.sol";
|
|
11
11
|
import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
|
package/src/CTPublisher.sol
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.26;
|
|
3
3
|
|
|
4
|
+
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
5
|
+
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
4
6
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
5
7
|
import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookStore.sol";
|
|
6
8
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
7
9
|
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
8
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
9
10
|
import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
|
|
10
11
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
11
12
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
13
|
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
13
14
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
14
15
|
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
16
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
15
17
|
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
16
18
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
17
|
-
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
18
|
-
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
19
19
|
|
|
20
20
|
import {ICTPublisher} from "./interfaces/ICTPublisher.sol";
|
|
21
21
|
import {CTAllowedPost} from "./structs/CTAllowedPost.sol";
|
|
@@ -27,6 +27,7 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
27
27
|
// --------------------------- custom errors ------------------------- //
|
|
28
28
|
//*********************************************************************//
|
|
29
29
|
|
|
30
|
+
error CTPublisher_DuplicatePost(bytes32 encodedIPFSUri);
|
|
30
31
|
error CTPublisher_EmptyEncodedIPFSUri();
|
|
31
32
|
error CTPublisher_InsufficientEthSent(uint256 expected, uint256 sent);
|
|
32
33
|
error CTPublisher_MaxTotalSupplyLessThanMin(uint256 min, uint256 max);
|
|
@@ -449,6 +450,13 @@ contract CTPublisher is JBPermissioned, ERC2771Context, ICTPublisher {
|
|
|
449
450
|
revert CTPublisher_EmptyEncodedIPFSUri();
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
// Check for duplicate encodedIPFSUri within the same batch to prevent fee evasion.
|
|
454
|
+
for (uint256 j; j < i; j++) {
|
|
455
|
+
if (posts[j].encodedIPFSUri == post.encodedIPFSUri) {
|
|
456
|
+
revert CTPublisher_DuplicatePost(post.encodedIPFSUri);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
452
460
|
// Scoped section to prevent stack too deep.
|
|
453
461
|
{
|
|
454
462
|
// Check if there's an ID of a tier already minted for this encodedIPFSUri.
|
|
@@ -5,24 +5,28 @@ import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721Tiers
|
|
|
5
5
|
import {IJB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHookDeployer.sol";
|
|
6
6
|
import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
|
|
7
7
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
8
|
-
|
|
9
|
-
import {ICTPublisher} from "./ICTPublisher.sol";
|
|
10
|
-
import {CTSuckerDeploymentConfig} from "../structs/CTSuckerDeploymentConfig.sol";
|
|
11
8
|
import {CTProjectConfig} from "../structs/CTProjectConfig.sol";
|
|
9
|
+
import {CTSuckerDeploymentConfig} from "../structs/CTSuckerDeploymentConfig.sol";
|
|
10
|
+
import {ICTPublisher} from "./ICTPublisher.sol";
|
|
12
11
|
|
|
12
|
+
/// @notice Deploys Croptop-configured Juicebox projects with tiered ERC-721 hooks.
|
|
13
13
|
interface ICTDeployer {
|
|
14
|
-
/// @notice The contract that mints ERC-721s representing Juicebox project ownership.
|
|
15
|
-
/// @return The projects contract.
|
|
16
|
-
function PROJECTS() external view returns (IJBProjects);
|
|
17
|
-
|
|
18
14
|
/// @notice The deployer used to launch tiered ERC-721 hook collections.
|
|
19
15
|
/// @return The hook deployer contract.
|
|
20
16
|
function DEPLOYER() external view returns (IJB721TiersHookDeployer);
|
|
21
17
|
|
|
18
|
+
/// @notice The contract that mints ERC-721s representing Juicebox project ownership.
|
|
19
|
+
/// @return The projects contract.
|
|
20
|
+
function PROJECTS() external view returns (IJBProjects);
|
|
21
|
+
|
|
22
22
|
/// @notice The Croptop publisher that manages posting criteria and minting.
|
|
23
23
|
/// @return The publisher contract.
|
|
24
24
|
function PUBLISHER() external view returns (ICTPublisher);
|
|
25
25
|
|
|
26
|
+
/// @notice Claim ownership of a tiered ERC-721 hook collection by transferring it to the project.
|
|
27
|
+
/// @param hook The hook to claim ownership of. The caller must own the project the hook belongs to.
|
|
28
|
+
function claimCollectionOwnershipOf(IJB721TiersHook hook) external;
|
|
29
|
+
|
|
26
30
|
/// @notice Deploy a simple Juicebox project configured to receive posts from Croptop templates.
|
|
27
31
|
/// @param owner The address that will own the project after deployment.
|
|
28
32
|
/// @param projectConfig The configuration for the project, including name, symbol, and allowed posts.
|
|
@@ -39,10 +43,6 @@ interface ICTDeployer {
|
|
|
39
43
|
external
|
|
40
44
|
returns (uint256 projectId, IJB721TiersHook hook);
|
|
41
45
|
|
|
42
|
-
/// @notice Claim ownership of a tiered ERC-721 hook collection by transferring it to the project.
|
|
43
|
-
/// @param hook The hook to claim ownership of. The caller must own the project the hook belongs to.
|
|
44
|
-
function claimCollectionOwnershipOf(IJB721TiersHook hook) external;
|
|
45
|
-
|
|
46
46
|
/// @notice Deploy new suckers for an existing project.
|
|
47
47
|
/// @param projectId The ID of the project to deploy suckers for.
|
|
48
48
|
/// @param suckerDeploymentConfiguration The suckers to set up for the project.
|
|
@@ -3,7 +3,6 @@ pragma solidity ^0.8.0;
|
|
|
3
3
|
|
|
4
4
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
5
5
|
import {IJBProjects} from "@bananapus/core-v6/src/interfaces/IJBProjects.sol";
|
|
6
|
-
|
|
7
6
|
import {ICTPublisher} from "./ICTPublisher.sol";
|
|
8
7
|
|
|
9
8
|
/// @notice A contract that can receive a Juicebox project NFT (via `safeTransferFrom`) and automatically grants the
|
|
@@ -4,12 +4,26 @@ pragma solidity ^0.8.0;
|
|
|
4
4
|
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
5
5
|
import {JB721Tier} from "@bananapus/721-hook-v6/src/structs/JB721Tier.sol";
|
|
6
6
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
-
|
|
8
7
|
import {CTAllowedPost} from "../structs/CTAllowedPost.sol";
|
|
9
8
|
import {CTPost} from "../structs/CTPost.sol";
|
|
10
9
|
|
|
10
|
+
/// @notice Manages posting criteria and mints NFTs for Croptop projects.
|
|
11
11
|
interface ICTPublisher {
|
|
12
|
+
/// @notice Emitted when posting criteria are configured for a hook.
|
|
13
|
+
/// @param hook The hook address the criteria apply to.
|
|
14
|
+
/// @param allowedPost The allowed post criteria that were configured.
|
|
15
|
+
/// @param caller The address that configured the criteria.
|
|
12
16
|
event ConfigurePostingCriteria(address indexed hook, CTAllowedPost allowedPost, address caller);
|
|
17
|
+
|
|
18
|
+
/// @notice Emitted when NFT posts are minted.
|
|
19
|
+
/// @param projectId The ID of the project the posts belong to.
|
|
20
|
+
/// @param hook The tiered ERC-721 hook the posts were minted from.
|
|
21
|
+
/// @param nftBeneficiary The address that received the minted NFTs.
|
|
22
|
+
/// @param feeBeneficiary The address that received fee project tokens.
|
|
23
|
+
/// @param posts The posts that were minted.
|
|
24
|
+
/// @param postValue The total value of the posts.
|
|
25
|
+
/// @param txValue The total value sent with the transaction.
|
|
26
|
+
/// @param caller The address that minted the posts.
|
|
13
27
|
event Mint(
|
|
14
28
|
uint256 indexed projectId,
|
|
15
29
|
IJB721TiersHook indexed hook,
|
|
@@ -21,24 +35,6 @@ interface ICTPublisher {
|
|
|
21
35
|
address caller
|
|
22
36
|
);
|
|
23
37
|
|
|
24
|
-
/// @notice The divisor that describes the fee percent. Equal to 100 divided by the fee percent.
|
|
25
|
-
/// @return The fee divisor.
|
|
26
|
-
function FEE_DIVISOR() external view returns (uint256);
|
|
27
|
-
|
|
28
|
-
/// @notice The directory that contains the projects being posted to.
|
|
29
|
-
/// @return The directory contract.
|
|
30
|
-
function DIRECTORY() external view returns (IJBDirectory);
|
|
31
|
-
|
|
32
|
-
/// @notice The ID of the project to which fees will be routed.
|
|
33
|
-
/// @return The fee project ID.
|
|
34
|
-
function FEE_PROJECT_ID() external view returns (uint256);
|
|
35
|
-
|
|
36
|
-
/// @notice The tier ID that an IPFS metadata URI has been saved to for a given hook.
|
|
37
|
-
/// @param hook The hook for which the tier ID applies.
|
|
38
|
-
/// @param encodedIPFSUri The encoded IPFS URI to look up.
|
|
39
|
-
/// @return The tier ID, or 0 if the URI has not been published.
|
|
40
|
-
function tierIdForEncodedIPFSUriOf(address hook, bytes32 encodedIPFSUri) external view returns (uint256);
|
|
41
|
-
|
|
42
38
|
/// @notice The post allowance for a particular category on a particular hook.
|
|
43
39
|
/// @param hook The hook contract for which this allowance applies.
|
|
44
40
|
/// @param category The category for which this allowance applies.
|
|
@@ -61,6 +57,24 @@ interface ICTPublisher {
|
|
|
61
57
|
address[] memory allowedAddresses
|
|
62
58
|
);
|
|
63
59
|
|
|
60
|
+
/// @notice The directory that contains the projects being posted to.
|
|
61
|
+
/// @return The directory contract.
|
|
62
|
+
function DIRECTORY() external view returns (IJBDirectory);
|
|
63
|
+
|
|
64
|
+
/// @notice The divisor that describes the fee percent. Equal to 100 divided by the fee percent.
|
|
65
|
+
/// @return The fee divisor.
|
|
66
|
+
function FEE_DIVISOR() external view returns (uint256);
|
|
67
|
+
|
|
68
|
+
/// @notice The ID of the project to which fees will be routed.
|
|
69
|
+
/// @return The fee project ID.
|
|
70
|
+
function FEE_PROJECT_ID() external view returns (uint256);
|
|
71
|
+
|
|
72
|
+
/// @notice The tier ID that an IPFS metadata URI has been saved to for a given hook.
|
|
73
|
+
/// @param hook The hook for which the tier ID applies.
|
|
74
|
+
/// @param encodedIPFSUri The encoded IPFS URI to look up.
|
|
75
|
+
/// @return The tier ID, or 0 if the URI has not been published.
|
|
76
|
+
function tierIdForEncodedIPFSUriOf(address hook, bytes32 encodedIPFSUri) external view returns (uint256);
|
|
77
|
+
|
|
64
78
|
/// @notice Get the tiers for the provided encoded IPFS URIs.
|
|
65
79
|
/// @param hook The hook from which to get tiers.
|
|
66
80
|
/// @param encodedIPFSUris The URIs to get tiers of.
|
package/test/Fork.t.sol
CHANGED
|
@@ -1,58 +1,107 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity ^0.8.17;
|
|
3
3
|
|
|
4
|
-
import "
|
|
5
|
-
import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
|
|
6
|
-
import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
|
|
4
|
+
import "forge-std/Test.sol";
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
// JB core — deploy fresh within fork.
|
|
7
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
8
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
9
|
+
import {JBDirectory} from "@bananapus/core-v6/src/JBDirectory.sol";
|
|
10
|
+
import {JBRulesets} from "@bananapus/core-v6/src/JBRulesets.sol";
|
|
11
|
+
import {JBTokens} from "@bananapus/core-v6/src/JBTokens.sol";
|
|
12
|
+
import {JBERC20} from "@bananapus/core-v6/src/JBERC20.sol";
|
|
13
|
+
import {JBSplits} from "@bananapus/core-v6/src/JBSplits.sol";
|
|
14
|
+
import {JBPrices} from "@bananapus/core-v6/src/JBPrices.sol";
|
|
15
|
+
import {JBController} from "@bananapus/core-v6/src/JBController.sol";
|
|
16
|
+
import {JBFundAccessLimits} from "@bananapus/core-v6/src/JBFundAccessLimits.sol";
|
|
9
17
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
18
|
+
|
|
19
|
+
// 721 hook — deploy fresh within fork.
|
|
20
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
21
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
22
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
23
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
24
|
+
|
|
25
|
+
// Suckers — deploy fresh within fork.
|
|
26
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
27
|
+
import {JBOptimismSuckerDeployer} from "@bananapus/suckers-v6/src/deployers/JBOptimismSuckerDeployer.sol";
|
|
28
|
+
import {JBOptimismSucker} from "@bananapus/suckers-v6/src/JBOptimismSucker.sol";
|
|
29
|
+
import {JBAddToBalanceMode} from "@bananapus/suckers-v6/src/enums/JBAddToBalanceMode.sol";
|
|
30
|
+
import {IOPMessenger} from "@bananapus/suckers-v6/src/interfaces/IOPMessenger.sol";
|
|
31
|
+
import {IOPStandardBridge} from "@bananapus/suckers-v6/src/interfaces/IOPStandardBridge.sol";
|
|
10
32
|
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
11
33
|
import {JBTokenMapping} from "@bananapus/suckers-v6/src/structs/JBTokenMapping.sol";
|
|
12
|
-
import {
|
|
13
|
-
import {CTPublisher} from "./../src/CTPublisher.sol";
|
|
34
|
+
import {IJBSuckerDeployer} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerDeployer.sol";
|
|
14
35
|
|
|
15
|
-
import
|
|
36
|
+
// Croptop — wildcard import pulls in all structs (CTProjectConfig, CTDeployerAllowedPost, etc.).
|
|
37
|
+
import "./../src/CTDeployer.sol";
|
|
38
|
+
import {CTPublisher} from "./../src/CTPublisher.sol";
|
|
16
39
|
|
|
40
|
+
/// @notice Fork tests for Croptop. Deploys all JB infrastructure fresh within a mainnet fork.
|
|
17
41
|
contract ForkTest is Test {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
42
|
+
// ───────────────────────── Mainnet addresses
|
|
43
|
+
// ──────────────────────────
|
|
44
|
+
|
|
45
|
+
// OP L1 bridge contracts (exist on Ethereum mainnet).
|
|
46
|
+
IOPMessenger constant OP_L1_MESSENGER = IOPMessenger(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1);
|
|
47
|
+
IOPStandardBridge constant OP_L1_BRIDGE = IOPStandardBridge(0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1);
|
|
48
|
+
|
|
49
|
+
// ───────────────────────── JB core (deployed fresh)
|
|
50
|
+
// ───────────────────
|
|
51
|
+
|
|
52
|
+
address multisig = address(0xBEEF);
|
|
53
|
+
address trustedForwarder = address(0);
|
|
54
|
+
|
|
55
|
+
JBPermissions jbPermissions;
|
|
56
|
+
JBProjects jbProjects;
|
|
57
|
+
JBDirectory jbDirectory;
|
|
58
|
+
JBRulesets jbRulesets;
|
|
59
|
+
JBTokens jbTokens;
|
|
60
|
+
JBSplits jbSplits;
|
|
61
|
+
JBPrices jbPrices;
|
|
62
|
+
JBFundAccessLimits jbFundAccessLimits;
|
|
63
|
+
JBController jbController;
|
|
64
|
+
|
|
65
|
+
// ───────────────────────── 721 hook (deployed fresh)
|
|
66
|
+
// ──────────────────
|
|
67
|
+
|
|
68
|
+
JB721TiersHookDeployer hookDeployer;
|
|
69
|
+
|
|
70
|
+
// ───────────────────────── Suckers (deployed fresh)
|
|
71
|
+
// ───────────────────
|
|
72
|
+
|
|
73
|
+
JBSuckerRegistry suckerRegistry;
|
|
74
|
+
JBOptimismSuckerDeployer opSuckerDeployer;
|
|
75
|
+
|
|
76
|
+
// ───────────────────────── Croptop
|
|
77
|
+
// ────────────────────────────────────
|
|
24
78
|
|
|
25
79
|
CTPublisher publisher;
|
|
26
80
|
CTDeployer deployer;
|
|
27
81
|
|
|
28
|
-
address TRUSTED_FORWARDER;
|
|
29
|
-
|
|
30
82
|
function setUp() public {
|
|
83
|
+
// Skip fork tests when the RPC URL is not available (e.g. in CI).
|
|
84
|
+
string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
|
|
85
|
+
if (bytes(rpcUrl).length == 0) {
|
|
86
|
+
vm.skip(true);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
31
90
|
// Fork ETH mainnet.
|
|
32
|
-
vm.createSelectFork(
|
|
91
|
+
vm.createSelectFork(rpcUrl);
|
|
33
92
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
);
|
|
39
|
-
// Get the deployment addresses for the 721 hook contracts for this chain.
|
|
40
|
-
hook = Hook721DeploymentLib.getDeployment(
|
|
41
|
-
vm.envOr("NANA_721_DEPLOYMENT_PATH", string("node_modules/@bananapus/721-hook-v6/deployments/"))
|
|
42
|
-
);
|
|
43
|
-
// Get the deployment addresses for the suckers contracts for this chain.
|
|
44
|
-
suckers = SuckerDeploymentLib.getDeployment(
|
|
45
|
-
vm.envOr("NANA_SUCKERS_DEPLOYMENT_PATH", string("node_modules/@bananapus/suckers-v6/deployments/"))
|
|
46
|
-
);
|
|
93
|
+
// Deploy all JB core contracts fresh within the fork.
|
|
94
|
+
_deployJBCore();
|
|
95
|
+
|
|
96
|
+
// Deploy the 721 hook infrastructure.
|
|
97
|
+
_deploy721Hook();
|
|
47
98
|
|
|
48
|
-
//
|
|
49
|
-
|
|
99
|
+
// Deploy the sucker infrastructure.
|
|
100
|
+
_deploySuckers();
|
|
50
101
|
|
|
51
102
|
// Deploy the croptop contracts.
|
|
52
|
-
publisher = new CTPublisher(
|
|
53
|
-
deployer = new CTDeployer(
|
|
54
|
-
core.permissions, core.projects, hook.hook_deployer, publisher, suckers.registry, TRUSTED_FORWARDER
|
|
55
|
-
);
|
|
103
|
+
publisher = new CTPublisher(jbDirectory, jbPermissions, 1, trustedForwarder);
|
|
104
|
+
deployer = new CTDeployer(jbPermissions, jbProjects, hookDeployer, publisher, suckerRegistry, trustedForwarder);
|
|
56
105
|
}
|
|
57
106
|
|
|
58
107
|
function testDeployProject(address owner) public {
|
|
@@ -72,7 +121,7 @@ contract ForkTest is Test {
|
|
|
72
121
|
CTSuckerDeploymentConfig memory suckerConfig =
|
|
73
122
|
CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
|
|
74
123
|
|
|
75
|
-
deployer.deployProjectFor(owner, config, suckerConfig,
|
|
124
|
+
deployer.deployProjectFor(owner, config, suckerConfig, jbController);
|
|
76
125
|
}
|
|
77
126
|
|
|
78
127
|
function testDeployProjectWithSuckers(address owner, bytes32 salt, bytes32 suckerSalt) public {
|
|
@@ -100,15 +149,79 @@ contract ForkTest is Test {
|
|
|
100
149
|
});
|
|
101
150
|
|
|
102
151
|
JBSuckerDeployerConfig[] memory deployerConfigurations = new JBSuckerDeployerConfig[](1);
|
|
103
|
-
deployerConfigurations[0] =
|
|
152
|
+
deployerConfigurations[0] =
|
|
153
|
+
JBSuckerDeployerConfig({deployer: IJBSuckerDeployer(address(opSuckerDeployer)), mappings: tokens});
|
|
104
154
|
|
|
105
155
|
CTSuckerDeploymentConfig memory suckerConfig =
|
|
106
156
|
CTSuckerDeploymentConfig({deployerConfigurations: deployerConfigurations, salt: suckerSalt});
|
|
107
157
|
|
|
108
158
|
// Deploy the project.
|
|
109
|
-
(uint256 projectId,) = deployer.deployProjectFor(owner, config, suckerConfig,
|
|
159
|
+
(uint256 projectId,) = deployer.deployProjectFor(owner, config, suckerConfig, jbController);
|
|
110
160
|
|
|
111
161
|
// Check that the projectId has a sucker.
|
|
112
|
-
assertEq(
|
|
162
|
+
assertEq(suckerRegistry.suckersOf(projectId).length, deployerConfigurations.length);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ───────────────────────── Internal deployment helpers
|
|
166
|
+
// ────────────────
|
|
167
|
+
|
|
168
|
+
function _deployJBCore() internal {
|
|
169
|
+
jbPermissions = new JBPermissions(trustedForwarder);
|
|
170
|
+
jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
|
|
171
|
+
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
172
|
+
JBERC20 jbErc20 = new JBERC20();
|
|
173
|
+
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
174
|
+
jbRulesets = new JBRulesets(jbDirectory);
|
|
175
|
+
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
|
|
176
|
+
jbSplits = new JBSplits(jbDirectory);
|
|
177
|
+
jbFundAccessLimits = new JBFundAccessLimits(jbDirectory);
|
|
178
|
+
|
|
179
|
+
jbController = new JBController(
|
|
180
|
+
jbDirectory,
|
|
181
|
+
jbFundAccessLimits,
|
|
182
|
+
jbPermissions,
|
|
183
|
+
jbPrices,
|
|
184
|
+
jbProjects,
|
|
185
|
+
jbRulesets,
|
|
186
|
+
jbSplits,
|
|
187
|
+
jbTokens,
|
|
188
|
+
address(0), // omnichainRulesetOperator
|
|
189
|
+
trustedForwarder
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
vm.prank(multisig);
|
|
193
|
+
jbDirectory.setIsAllowedToSetFirstController(address(jbController), true);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _deploy721Hook() internal {
|
|
197
|
+
JB721TiersHookStore store = new JB721TiersHookStore();
|
|
198
|
+
JBAddressRegistry addressRegistry = new JBAddressRegistry();
|
|
199
|
+
|
|
200
|
+
JB721TiersHook hookImpl =
|
|
201
|
+
new JB721TiersHook(jbDirectory, jbPermissions, jbRulesets, store, jbSplits, trustedForwarder);
|
|
202
|
+
|
|
203
|
+
hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _deploySuckers() internal {
|
|
207
|
+
suckerRegistry = new JBSuckerRegistry(jbDirectory, jbPermissions, multisig, trustedForwarder);
|
|
208
|
+
|
|
209
|
+
// Deploy the OP sucker deployer with `multisig` as the configurator.
|
|
210
|
+
opSuckerDeployer =
|
|
211
|
+
new JBOptimismSuckerDeployer(jbDirectory, jbPermissions, jbTokens, multisig, trustedForwarder);
|
|
212
|
+
|
|
213
|
+
// Configure the OP sucker deployer with L1 bridge addresses.
|
|
214
|
+
vm.startPrank(multisig);
|
|
215
|
+
opSuckerDeployer.setChainSpecificConstants(OP_L1_MESSENGER, OP_L1_BRIDGE);
|
|
216
|
+
|
|
217
|
+
// Deploy and configure the singleton.
|
|
218
|
+
JBOptimismSucker singleton = new JBOptimismSucker(
|
|
219
|
+
opSuckerDeployer, jbDirectory, jbPermissions, jbTokens, JBAddToBalanceMode.ON_CLAIM, trustedForwarder
|
|
220
|
+
);
|
|
221
|
+
opSuckerDeployer.configureSingleton(singleton);
|
|
222
|
+
|
|
223
|
+
// Allow the deployer in the registry.
|
|
224
|
+
suckerRegistry.allowSuckerDeployer(address(opSuckerDeployer));
|
|
225
|
+
vm.stopPrank();
|
|
113
226
|
}
|
|
114
227
|
}
|
|
@@ -20,7 +20,7 @@ import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
|
|
|
20
20
|
import {CTPost} from "../../src/structs/CTPost.sol";
|
|
21
21
|
|
|
22
22
|
/// @title H19_FeeEvasion
|
|
23
|
-
/// @notice
|
|
23
|
+
/// @notice Fee evasion for existing tier mints.
|
|
24
24
|
/// Before the fix, a user could set post.price = 0 for an existing tier
|
|
25
25
|
/// to evade the 5% Croptop fee entirely. The fix reads the actual tier price
|
|
26
26
|
/// from the store for existing tiers.
|
|
@@ -19,7 +19,7 @@ import {CTAllowedPost} from "../../src/structs/CTAllowedPost.sol";
|
|
|
19
19
|
import {CTPost} from "../../src/structs/CTPost.sol";
|
|
20
20
|
|
|
21
21
|
/// @title L52_StaleTierIdMapping
|
|
22
|
-
/// @notice
|
|
22
|
+
/// @notice Stale tierIdForEncodedIPFSUriOf mapping after external tier removal.
|
|
23
23
|
/// When a tier is removed externally via adjustTiers(), the publisher's mapping still pointed
|
|
24
24
|
/// to the removed tier ID, blocking re-creation. The fix clears the stale mapping and allows
|
|
25
25
|
/// the post to fall through to new-tier creation.
|
|
@@ -159,7 +159,7 @@ contract L52_StaleTierIdMapping is Test {
|
|
|
159
159
|
abi.encode(false)
|
|
160
160
|
);
|
|
161
161
|
|
|
162
|
-
// Mock tierOf for tier 1 so the
|
|
162
|
+
// Mock tierOf for tier 1 so the existing-tier price lookup succeeds.
|
|
163
163
|
JB721Tier memory tier = JB721Tier({
|
|
164
164
|
id: 1,
|
|
165
165
|
price: 0.1 ether,
|