@croptop/core-v6 0.0.18 → 0.0.20
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/CHANGE_LOG.md +2 -2
- package/package.json +8 -8
- package/test/CTPublisher.t.sol +59 -0
- package/test/fork/PublishFork.t.sol +437 -0
- package/deployments/croptop-core-v5/arbitrum/CTDeployer.json +0 -1896
- package/deployments/croptop-core-v5/arbitrum/CTProjectOwner.json +0 -186
- package/deployments/croptop-core-v5/arbitrum/CTPublisher.json +0 -738
- package/deployments/croptop-core-v5/arbitrum_sepolia/CTDeployer.json +0 -1883
- package/deployments/croptop-core-v5/arbitrum_sepolia/CTProjectOwner.json +0 -186
- package/deployments/croptop-core-v5/arbitrum_sepolia/CTPublisher.json +0 -738
- package/deployments/croptop-core-v5/base/CTDeployer.json +0 -1908
- package/deployments/croptop-core-v5/base/CTProjectOwner.json +0 -190
- package/deployments/croptop-core-v5/base/CTPublisher.json +0 -741
- package/deployments/croptop-core-v5/base_sepolia/CTDeployer.json +0 -1894
- package/deployments/croptop-core-v5/base_sepolia/CTProjectOwner.json +0 -190
- package/deployments/croptop-core-v5/base_sepolia/CTPublisher.json +0 -741
- package/deployments/croptop-core-v5/ethereum/CTDeployer.json +0 -1894
- package/deployments/croptop-core-v5/ethereum/CTProjectOwner.json +0 -190
- package/deployments/croptop-core-v5/ethereum/CTPublisher.json +0 -741
- package/deployments/croptop-core-v5/optimism/CTDeployer.json +0 -1894
- package/deployments/croptop-core-v5/optimism/CTProjectOwner.json +0 -190
- package/deployments/croptop-core-v5/optimism/CTPublisher.json +0 -741
- package/deployments/croptop-core-v5/optimism_sepolia/CTDeployer.json +0 -1894
- package/deployments/croptop-core-v5/optimism_sepolia/CTProjectOwner.json +0 -190
- package/deployments/croptop-core-v5/optimism_sepolia/CTPublisher.json +0 -741
- package/deployments/croptop-core-v5/sepolia/CTDeployer.json +0 -1894
- package/deployments/croptop-core-v5/sepolia/CTProjectOwner.json +0 -190
- package/deployments/croptop-core-v5/sepolia/CTPublisher.json +0 -741
package/CHANGE_LOG.md
CHANGED
|
@@ -86,8 +86,8 @@ Posts can now include a `splitPercent` and an array of `splits` (JBSplit[]) that
|
|
|
86
86
|
- **v5:** When minting from an existing tier, `totalPrice` was accumulated using `post.price` (user-supplied).
|
|
87
87
|
- **v6:** When minting from an existing tier, `totalPrice` is accumulated using the actual tier price fetched from `store.tierOf()`. This prevents a caller from passing `price=0` for an existing tier to evade fees.
|
|
88
88
|
|
|
89
|
-
### CTDeployer
|
|
90
|
-
- The CTDeployer
|
|
89
|
+
### CTDeployer Data Hook Proxy Activated
|
|
90
|
+
- The CTDeployer implemented the data hook proxy pattern in v5 as well -- it had `beforePayRecordedWith`, `beforeCashOutRecordedWith`, `hasMintPermissionFor`, and the `dataHookOf` mapping -- but `deployProjectFor` set `metadata.dataHook = address(hook)` (the 721 hook directly), so the proxy methods were never called. In v6, `deployProjectFor` sets `metadata.dataHook = address(this)`, `cashOutTaxRate = MAX_CASH_OUT_TAX_RATE`, and `useDataHookForCashOut = true`, activating the proxy. This routes all pay and cash out data hook calls through CTDeployer, which forwards them to the stored `dataHookOf[projectId]` while intercepting sucker cash outs (verified via `SUCKER_REGISTRY.isSuckerOf`) to return a 0% tax rate for cross-chain operations.
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@croptop/core-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.20",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,17 +16,17 @@
|
|
|
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.
|
|
22
|
-
"@bananapus/ownable-v6": "^0.0.
|
|
19
|
+
"@bananapus/721-hook-v6": "^0.0.19",
|
|
20
|
+
"@bananapus/buyback-hook-v6": "^0.0.16",
|
|
21
|
+
"@bananapus/core-v6": "^0.0.24",
|
|
22
|
+
"@bananapus/ownable-v6": "^0.0.12",
|
|
23
23
|
"@bananapus/permission-ids-v6": "^0.0.10",
|
|
24
|
-
"@bananapus/router-terminal-v6": "^0.0.
|
|
25
|
-
"@bananapus/suckers-v6": "^0.0.
|
|
24
|
+
"@bananapus/router-terminal-v6": "^0.0.16",
|
|
25
|
+
"@bananapus/suckers-v6": "^0.0.13",
|
|
26
26
|
"@openzeppelin/contracts": "^5.6.1"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@rev-net/core-v6": "^0.0.
|
|
29
|
+
"@rev-net/core-v6": "^0.0.14",
|
|
30
30
|
"@sphinx-labs/plugins": "^0.33.1"
|
|
31
31
|
}
|
|
32
32
|
}
|
package/test/CTPublisher.t.sol
CHANGED
|
@@ -14,6 +14,8 @@ import {IJB721TiersHookStore} from "@bananapus/721-hook-v6/src/interfaces/IJB721
|
|
|
14
14
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
15
15
|
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
16
16
|
|
|
17
|
+
import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
|
|
18
|
+
|
|
17
19
|
import {CTPublisher} from "../src/CTPublisher.sol";
|
|
18
20
|
import {CTAllowedPost} from "../src/structs/CTAllowedPost.sol";
|
|
19
21
|
import {CTPost} from "../src/structs/CTPost.sol";
|
|
@@ -770,6 +772,63 @@ contract TestCTPublisher is Test {
|
|
|
770
772
|
// --- Multiple Posts With Different Split Percents ------------------- //
|
|
771
773
|
//*********************************************************************//
|
|
772
774
|
|
|
775
|
+
function test_mintFrom_nonzeroSplitPercent_passesSplitsToTier() public {
|
|
776
|
+
_configureCategoryWithSplits(5, 0.01 ether, 1, 100, 500_000_000);
|
|
777
|
+
_setupMintMocks();
|
|
778
|
+
|
|
779
|
+
address splitBeneficiary = makeAddr("splitBeneficiary");
|
|
780
|
+
|
|
781
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
782
|
+
splits[0] = JBSplit({
|
|
783
|
+
percent: 500_000_000, // 50% of tier revenue to beneficiary
|
|
784
|
+
projectId: 0,
|
|
785
|
+
beneficiary: payable(splitBeneficiary),
|
|
786
|
+
preferAddToBalance: false,
|
|
787
|
+
lockedUntil: 0,
|
|
788
|
+
hook: IJBSplitHook(address(0))
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
CTPost[] memory posts = new CTPost[](1);
|
|
792
|
+
posts[0] = CTPost({
|
|
793
|
+
encodedIPFSUri: keccak256("split-beneficiary-test"),
|
|
794
|
+
totalSupply: 10,
|
|
795
|
+
price: 0.1 ether,
|
|
796
|
+
category: 5,
|
|
797
|
+
splitPercent: 250_000_000, // 25% split
|
|
798
|
+
splits: splits
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
// Build expected tier config to verify splits are passed through.
|
|
802
|
+
JB721TierConfig[] memory expectedTiers = new JB721TierConfig[](1);
|
|
803
|
+
expectedTiers[0] = JB721TierConfig({
|
|
804
|
+
price: 0.1 ether,
|
|
805
|
+
initialSupply: 10,
|
|
806
|
+
votingUnits: 0,
|
|
807
|
+
reserveFrequency: 0,
|
|
808
|
+
reserveBeneficiary: address(0),
|
|
809
|
+
encodedIPFSUri: keccak256("split-beneficiary-test"),
|
|
810
|
+
category: 5,
|
|
811
|
+
discountPercent: 0,
|
|
812
|
+
allowOwnerMint: false,
|
|
813
|
+
useReserveBeneficiaryAsDefault: false,
|
|
814
|
+
transfersPausable: false,
|
|
815
|
+
useVotingUnits: true,
|
|
816
|
+
cannotBeRemoved: false,
|
|
817
|
+
cannotIncreaseDiscountPercent: false,
|
|
818
|
+
splitPercent: 250_000_000,
|
|
819
|
+
splits: splits
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Verify adjustTiers receives the tier config with the correct split beneficiary and percent.
|
|
823
|
+
vm.expectCall(
|
|
824
|
+
hookAddr, abi.encodeWithSelector(IJB721TiersHook.adjustTiers.selector, expectedTiers, new uint256[](0))
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
uint256 fee = 0.1 ether / 20;
|
|
828
|
+
vm.prank(poster);
|
|
829
|
+
publisher.mintFrom{value: 0.1 ether + fee}(IJB721TiersHook(hookAddr), posts, poster, poster, "", "");
|
|
830
|
+
}
|
|
831
|
+
|
|
773
832
|
function test_mintFrom_multiplePostsDifferentSplits() public {
|
|
774
833
|
// Category 5 allows up to 50% splits.
|
|
775
834
|
_configureCategoryWithSplits(5, 0, 1, 100, 500_000_000);
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.17;
|
|
3
|
+
|
|
4
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
5
|
+
import "forge-std/Test.sol";
|
|
6
|
+
|
|
7
|
+
// JB core — deploy fresh within fork.
|
|
8
|
+
import {JBPermissions} from "@bananapus/core-v6/src/JBPermissions.sol";
|
|
9
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
10
|
+
import {JBDirectory} from "@bananapus/core-v6/src/JBDirectory.sol";
|
|
11
|
+
import {JBRulesets} from "@bananapus/core-v6/src/JBRulesets.sol";
|
|
12
|
+
import {JBTokens} from "@bananapus/core-v6/src/JBTokens.sol";
|
|
13
|
+
import {JBERC20} from "@bananapus/core-v6/src/JBERC20.sol";
|
|
14
|
+
import {JBSplits} from "@bananapus/core-v6/src/JBSplits.sol";
|
|
15
|
+
import {JBPrices} from "@bananapus/core-v6/src/JBPrices.sol";
|
|
16
|
+
import {JBController} from "@bananapus/core-v6/src/JBController.sol";
|
|
17
|
+
import {JBFundAccessLimits} from "@bananapus/core-v6/src/JBFundAccessLimits.sol";
|
|
18
|
+
import {JBMultiTerminal} from "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
19
|
+
import {JBTerminalStore} from "@bananapus/core-v6/src/JBTerminalStore.sol";
|
|
20
|
+
import {JBFeelessAddresses} from "@bananapus/core-v6/src/JBFeelessAddresses.sol";
|
|
21
|
+
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
22
|
+
import {JBCurrencyIds} from "@bananapus/core-v6/src/libraries/JBCurrencyIds.sol";
|
|
23
|
+
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
24
|
+
|
|
25
|
+
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
26
|
+
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
27
|
+
import {JBRulesetConfig} from "@bananapus/core-v6/src/structs/JBRulesetConfig.sol";
|
|
28
|
+
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
29
|
+
import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
|
|
30
|
+
|
|
31
|
+
// 721 hook — deploy fresh within fork.
|
|
32
|
+
import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
|
|
33
|
+
import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
|
|
34
|
+
import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
|
|
35
|
+
import {IJB721TiersHook} from "@bananapus/721-hook-v6/src/interfaces/IJB721TiersHook.sol";
|
|
36
|
+
import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
37
|
+
import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
38
|
+
|
|
39
|
+
// Suckers — deploy fresh within fork.
|
|
40
|
+
import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
|
|
41
|
+
import {JBOptimismSuckerDeployer} from "@bananapus/suckers-v6/src/deployers/JBOptimismSuckerDeployer.sol";
|
|
42
|
+
import {JBOptimismSucker} from "@bananapus/suckers-v6/src/JBOptimismSucker.sol";
|
|
43
|
+
import {IOPMessenger} from "@bananapus/suckers-v6/src/interfaces/IOPMessenger.sol";
|
|
44
|
+
import {IOPStandardBridge} from "@bananapus/suckers-v6/src/interfaces/IOPStandardBridge.sol";
|
|
45
|
+
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
46
|
+
|
|
47
|
+
// Permit2
|
|
48
|
+
import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
|
|
49
|
+
import {DeployPermit2} from "@uniswap/permit2/test/utils/DeployPermit2.sol";
|
|
50
|
+
|
|
51
|
+
// Croptop
|
|
52
|
+
// forge-lint: disable-next-line(unaliased-plain-import)
|
|
53
|
+
import "./../../src/CTDeployer.sol";
|
|
54
|
+
import {CTPublisher} from "./../../src/CTPublisher.sol";
|
|
55
|
+
import {CTPost} from "./../../src/structs/CTPost.sol";
|
|
56
|
+
|
|
57
|
+
/// @notice Fork tests for CTPublisher.mintFrom(). Deploys all JB infrastructure fresh within a mainnet fork,
|
|
58
|
+
/// then exercises the publish-and-mint flow end-to-end.
|
|
59
|
+
contract PublishForkTest is Test, DeployPermit2 {
|
|
60
|
+
// ───────────────────────── Mainnet addresses
|
|
61
|
+
// ──────────────────────────
|
|
62
|
+
|
|
63
|
+
IOPMessenger constant OP_L1_MESSENGER = IOPMessenger(0x25ace71c97B33Cc4729CF772ae268934F7ab5fA1);
|
|
64
|
+
IOPStandardBridge constant OP_L1_BRIDGE = IOPStandardBridge(0x99C9fc46f92E8a1c0deC1b1747d010903E884bE1);
|
|
65
|
+
|
|
66
|
+
// ───────────────────────── JB core (deployed fresh)
|
|
67
|
+
// ───────────────────
|
|
68
|
+
|
|
69
|
+
address multisig = address(0xBEEF);
|
|
70
|
+
address trustedForwarder = address(0);
|
|
71
|
+
|
|
72
|
+
JBPermissions jbPermissions;
|
|
73
|
+
JBProjects jbProjects;
|
|
74
|
+
JBDirectory jbDirectory;
|
|
75
|
+
JBRulesets jbRulesets;
|
|
76
|
+
JBTokens jbTokens;
|
|
77
|
+
JBSplits jbSplits;
|
|
78
|
+
JBPrices jbPrices;
|
|
79
|
+
JBFundAccessLimits jbFundAccessLimits;
|
|
80
|
+
JBController jbController;
|
|
81
|
+
|
|
82
|
+
// Terminal infrastructure.
|
|
83
|
+
JBFeelessAddresses jbFeelessAddresses;
|
|
84
|
+
JBTerminalStore jbTerminalStore;
|
|
85
|
+
JBMultiTerminal jbMultiTerminal;
|
|
86
|
+
|
|
87
|
+
// ───────────────────────── 721 hook (deployed fresh)
|
|
88
|
+
// ──────────────────
|
|
89
|
+
|
|
90
|
+
JB721TiersHookDeployer hookDeployer;
|
|
91
|
+
|
|
92
|
+
// ───────────────────────── Suckers (deployed fresh)
|
|
93
|
+
// ───────────────────
|
|
94
|
+
|
|
95
|
+
JBSuckerRegistry suckerRegistry;
|
|
96
|
+
JBOptimismSuckerDeployer opSuckerDeployer;
|
|
97
|
+
|
|
98
|
+
// ───────────────────────── Croptop
|
|
99
|
+
// ────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
CTPublisher publisher;
|
|
102
|
+
CTDeployer deployer;
|
|
103
|
+
|
|
104
|
+
// ───────────────────────── Test actors & state
|
|
105
|
+
// ────────────────────────
|
|
106
|
+
|
|
107
|
+
address projectOwner = address(0xA11CE);
|
|
108
|
+
address poster = address(0xB0B);
|
|
109
|
+
address nftBeneficiary = address(0xCAFE);
|
|
110
|
+
address feeBeneficiary = address(0xFEE);
|
|
111
|
+
|
|
112
|
+
uint256 feeProjectId; // project 1
|
|
113
|
+
uint256 testProjectId;
|
|
114
|
+
IJB721TiersHook testHook;
|
|
115
|
+
|
|
116
|
+
// ───────────────────────── Constants
|
|
117
|
+
// ──────────────────────────────────
|
|
118
|
+
|
|
119
|
+
uint104 constant POST_PRICE = 0.1 ether;
|
|
120
|
+
uint32 constant POST_SUPPLY = 100;
|
|
121
|
+
uint24 constant POST_CATEGORY = 1;
|
|
122
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
123
|
+
bytes32 constant TEST_URI = bytes32("test_ipfs_uri");
|
|
124
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
125
|
+
bytes32 constant TEST_URI_2 = bytes32("test_ipfs_uri_2");
|
|
126
|
+
|
|
127
|
+
// ───────────────────────── Setup
|
|
128
|
+
// ─────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
function setUp() public {
|
|
131
|
+
// Fork ETH mainnet.
|
|
132
|
+
vm.createSelectFork("ethereum");
|
|
133
|
+
|
|
134
|
+
// Deploy all JB core contracts fresh within the fork.
|
|
135
|
+
_deployJBCore();
|
|
136
|
+
|
|
137
|
+
// CTDeployer hardcodes baseCurrency = JBCurrencyIds.ETH (1), but the accounting context
|
|
138
|
+
// uses currency = uint32(uint160(NATIVE_TOKEN)) = 61166. Add an identity price feed
|
|
139
|
+
// so JBPrices can convert between them.
|
|
140
|
+
MockPriceFeed identityFeed = new MockPriceFeed(1e18, 18);
|
|
141
|
+
vm.prank(multisig);
|
|
142
|
+
jbPrices.addPriceFeedFor({
|
|
143
|
+
projectId: 0,
|
|
144
|
+
pricingCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
145
|
+
unitCurrency: JBCurrencyIds.ETH,
|
|
146
|
+
feed: identityFeed
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Deploy the terminal infrastructure.
|
|
150
|
+
_deployTerminal();
|
|
151
|
+
|
|
152
|
+
// Deploy the 721 hook infrastructure.
|
|
153
|
+
_deploy721Hook();
|
|
154
|
+
|
|
155
|
+
// Deploy the sucker infrastructure.
|
|
156
|
+
_deploySuckers();
|
|
157
|
+
|
|
158
|
+
// Deploy the croptop contracts.
|
|
159
|
+
publisher = new CTPublisher(jbDirectory, jbPermissions, 1, trustedForwarder);
|
|
160
|
+
deployer = new CTDeployer(jbPermissions, jbProjects, hookDeployer, publisher, suckerRegistry, trustedForwarder);
|
|
161
|
+
|
|
162
|
+
// Launch the fee project (project 1) with a terminal that accepts ETH.
|
|
163
|
+
feeProjectId = _launchFeeProject();
|
|
164
|
+
|
|
165
|
+
// Launch a test project via CTDeployer with a terminal + allowed posts.
|
|
166
|
+
(testProjectId, testHook) = _launchTestProject();
|
|
167
|
+
|
|
168
|
+
// Fund the poster.
|
|
169
|
+
vm.deal(poster, 10 ether);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ───────────────────────── Tests
|
|
173
|
+
// ─────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/// @notice Verify that mintFrom() mints an NFT to the specified beneficiary.
|
|
176
|
+
function testFork_MintFromPublishesNFT() public {
|
|
177
|
+
// Build a valid post.
|
|
178
|
+
CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
|
|
179
|
+
|
|
180
|
+
// Calculate required msg.value: price + fee.
|
|
181
|
+
uint256 fee = uint256(POST_PRICE) / 20;
|
|
182
|
+
uint256 totalValue = uint256(POST_PRICE) + fee;
|
|
183
|
+
|
|
184
|
+
// Check NFT balance before.
|
|
185
|
+
uint256 balanceBefore = IERC721(address(testHook)).balanceOf(nftBeneficiary);
|
|
186
|
+
|
|
187
|
+
// Mint.
|
|
188
|
+
vm.prank(poster);
|
|
189
|
+
publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
|
|
190
|
+
|
|
191
|
+
// Verify NFT was minted to the beneficiary.
|
|
192
|
+
uint256 balanceAfter = IERC721(address(testHook)).balanceOf(nftBeneficiary);
|
|
193
|
+
assertEq(balanceAfter, balanceBefore + 1, "NFT should be minted to beneficiary");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/// @notice Verify 5% fee is routed to fee project and the rest to the test project.
|
|
197
|
+
function testFork_MintFromFeeDistribution() public {
|
|
198
|
+
CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
|
|
199
|
+
|
|
200
|
+
uint256 fee = uint256(POST_PRICE) / 20;
|
|
201
|
+
uint256 totalValue = uint256(POST_PRICE) + fee;
|
|
202
|
+
|
|
203
|
+
// Record terminal balances before minting.
|
|
204
|
+
uint256 feeProjectBalanceBefore =
|
|
205
|
+
jbTerminalStore.balanceOf(address(jbMultiTerminal), feeProjectId, JBConstants.NATIVE_TOKEN);
|
|
206
|
+
uint256 testProjectBalanceBefore =
|
|
207
|
+
jbTerminalStore.balanceOf(address(jbMultiTerminal), testProjectId, JBConstants.NATIVE_TOKEN);
|
|
208
|
+
|
|
209
|
+
// Mint.
|
|
210
|
+
vm.prank(poster);
|
|
211
|
+
publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
|
|
212
|
+
|
|
213
|
+
// Verify fee project terminal balance increased by the fee amount.
|
|
214
|
+
uint256 feeProjectBalanceAfter =
|
|
215
|
+
jbTerminalStore.balanceOf(address(jbMultiTerminal), feeProjectId, JBConstants.NATIVE_TOKEN);
|
|
216
|
+
assertEq(
|
|
217
|
+
feeProjectBalanceAfter - feeProjectBalanceBefore,
|
|
218
|
+
fee,
|
|
219
|
+
"Fee project balance should increase by totalPrice / 20"
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
// Verify test project terminal balance increased by the post price.
|
|
223
|
+
uint256 testProjectBalanceAfter =
|
|
224
|
+
jbTerminalStore.balanceOf(address(jbMultiTerminal), testProjectId, JBConstants.NATIVE_TOKEN);
|
|
225
|
+
assertEq(
|
|
226
|
+
testProjectBalanceAfter - testProjectBalanceBefore,
|
|
227
|
+
uint256(POST_PRICE),
|
|
228
|
+
"Test project balance should increase by post price"
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// @notice Verify that sending less ETH than required reverts.
|
|
233
|
+
function testFork_MintFromInsufficientFeeReverts() public {
|
|
234
|
+
CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
|
|
235
|
+
|
|
236
|
+
// Send only the post price, not the post price + fee.
|
|
237
|
+
uint256 insufficientValue = uint256(POST_PRICE);
|
|
238
|
+
|
|
239
|
+
vm.prank(poster);
|
|
240
|
+
vm.expectRevert();
|
|
241
|
+
publisher.mintFrom{value: insufficientValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// @notice Verify that minting the same encodedIPFSUri twice reuses the existing tier ID.
|
|
245
|
+
function testFork_MintFromDuplicatePostReusesExistingTier() public {
|
|
246
|
+
CTPost[] memory posts = _singlePost(TEST_URI, POST_PRICE, POST_SUPPLY, POST_CATEGORY);
|
|
247
|
+
|
|
248
|
+
uint256 fee = uint256(POST_PRICE) / 20;
|
|
249
|
+
uint256 totalValue = uint256(POST_PRICE) + fee;
|
|
250
|
+
|
|
251
|
+
// First mint.
|
|
252
|
+
vm.prank(poster);
|
|
253
|
+
publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
|
|
254
|
+
|
|
255
|
+
// Record the tier ID assigned to this URI after the first mint.
|
|
256
|
+
uint256 tierIdAfterFirst = publisher.tierIdForEncodedIPFSUriOf(address(testHook), TEST_URI);
|
|
257
|
+
assertGt(tierIdAfterFirst, 0, "Tier ID should be non-zero after first mint");
|
|
258
|
+
|
|
259
|
+
// Second mint with the same URI. The existing tier should be reused.
|
|
260
|
+
vm.prank(poster);
|
|
261
|
+
publisher.mintFrom{value: totalValue}(testHook, posts, nftBeneficiary, feeBeneficiary, "", "");
|
|
262
|
+
|
|
263
|
+
// Verify the tier ID is unchanged — no new tier was created.
|
|
264
|
+
uint256 tierIdAfterSecond = publisher.tierIdForEncodedIPFSUriOf(address(testHook), TEST_URI);
|
|
265
|
+
assertEq(tierIdAfterFirst, tierIdAfterSecond, "Tier ID should be reused for duplicate encodedIPFSUri");
|
|
266
|
+
|
|
267
|
+
// Verify two NFTs were minted total.
|
|
268
|
+
assertEq(IERC721(address(testHook)).balanceOf(nftBeneficiary), 2, "Two NFTs should be minted across both calls");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ───────────────────────── Internal deployment helpers
|
|
272
|
+
// ────────────────
|
|
273
|
+
|
|
274
|
+
// forge-lint: disable-next-line(mixed-case-function)
|
|
275
|
+
function _deployJBCore() internal {
|
|
276
|
+
jbPermissions = new JBPermissions(trustedForwarder);
|
|
277
|
+
jbProjects = new JBProjects(multisig, address(0), trustedForwarder);
|
|
278
|
+
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
279
|
+
JBERC20 jbErc20 = new JBERC20();
|
|
280
|
+
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
281
|
+
jbRulesets = new JBRulesets(jbDirectory);
|
|
282
|
+
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, trustedForwarder);
|
|
283
|
+
jbSplits = new JBSplits(jbDirectory);
|
|
284
|
+
jbFundAccessLimits = new JBFundAccessLimits(jbDirectory);
|
|
285
|
+
|
|
286
|
+
jbController = new JBController(
|
|
287
|
+
jbDirectory,
|
|
288
|
+
jbFundAccessLimits,
|
|
289
|
+
jbPermissions,
|
|
290
|
+
jbPrices,
|
|
291
|
+
jbProjects,
|
|
292
|
+
jbRulesets,
|
|
293
|
+
jbSplits,
|
|
294
|
+
jbTokens,
|
|
295
|
+
address(0), // omnichainRulesetOperator
|
|
296
|
+
trustedForwarder
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
vm.prank(multisig);
|
|
300
|
+
jbDirectory.setIsAllowedToSetFirstController(address(jbController), true);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _deployTerminal() internal {
|
|
304
|
+
jbFeelessAddresses = new JBFeelessAddresses(multisig);
|
|
305
|
+
jbTerminalStore = new JBTerminalStore(jbDirectory, jbPrices, jbRulesets);
|
|
306
|
+
|
|
307
|
+
address permit2 = deployPermit2();
|
|
308
|
+
|
|
309
|
+
jbMultiTerminal = new JBMultiTerminal(
|
|
310
|
+
jbFeelessAddresses,
|
|
311
|
+
jbPermissions,
|
|
312
|
+
jbProjects,
|
|
313
|
+
jbSplits,
|
|
314
|
+
jbTerminalStore,
|
|
315
|
+
jbTokens,
|
|
316
|
+
IPermit2(permit2),
|
|
317
|
+
trustedForwarder
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function _deploy721Hook() internal {
|
|
322
|
+
JB721TiersHookStore store = new JB721TiersHookStore();
|
|
323
|
+
JBAddressRegistry addressRegistry = new JBAddressRegistry();
|
|
324
|
+
|
|
325
|
+
JB721TiersHook hookImpl =
|
|
326
|
+
new JB721TiersHook(jbDirectory, jbPermissions, jbPrices, jbRulesets, store, jbSplits, trustedForwarder);
|
|
327
|
+
|
|
328
|
+
hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, trustedForwarder);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function _deploySuckers() internal {
|
|
332
|
+
suckerRegistry = new JBSuckerRegistry(jbDirectory, jbPermissions, multisig, trustedForwarder);
|
|
333
|
+
|
|
334
|
+
opSuckerDeployer =
|
|
335
|
+
new JBOptimismSuckerDeployer(jbDirectory, jbPermissions, jbTokens, multisig, trustedForwarder);
|
|
336
|
+
|
|
337
|
+
vm.startPrank(multisig);
|
|
338
|
+
opSuckerDeployer.setChainSpecificConstants(OP_L1_MESSENGER, OP_L1_BRIDGE);
|
|
339
|
+
|
|
340
|
+
JBOptimismSucker singleton = new JBOptimismSucker(
|
|
341
|
+
opSuckerDeployer, jbDirectory, jbPermissions, jbTokens, 1, suckerRegistry, trustedForwarder
|
|
342
|
+
);
|
|
343
|
+
opSuckerDeployer.configureSingleton(singleton);
|
|
344
|
+
|
|
345
|
+
suckerRegistry.allowSuckerDeployer(address(opSuckerDeployer));
|
|
346
|
+
vm.stopPrank();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/// @notice Launch fee project (project 1) with ETH terminal so it can receive fees.
|
|
350
|
+
function _launchFeeProject() internal returns (uint256 projectId) {
|
|
351
|
+
// Build terminal config accepting native ETH.
|
|
352
|
+
JBTerminalConfig[] memory terminalConfigs = _ethTerminalConfig();
|
|
353
|
+
|
|
354
|
+
// A simple ruleset with no special rules.
|
|
355
|
+
JBRulesetConfig[] memory rulesetConfigs = new JBRulesetConfig[](1);
|
|
356
|
+
rulesetConfigs[0].weight = 1_000_000 * (10 ** 18);
|
|
357
|
+
rulesetConfigs[0].metadata.baseCurrency = JBCurrencyIds.ETH;
|
|
358
|
+
|
|
359
|
+
projectId = jbController.launchProjectFor({
|
|
360
|
+
owner: multisig,
|
|
361
|
+
projectUri: "Fee Project",
|
|
362
|
+
rulesetConfigurations: rulesetConfigs,
|
|
363
|
+
terminalConfigurations: terminalConfigs,
|
|
364
|
+
memo: "Fee project launch"
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// Sanity check: fee project must be project 1.
|
|
368
|
+
assertEq(projectId, 1, "Fee project must be project ID 1");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/// @notice Launch a test project via CTDeployer with ETH terminal and allowed posts.
|
|
372
|
+
function _launchTestProject() internal returns (uint256 projectId, IJB721TiersHook hook) {
|
|
373
|
+
// Build terminal config accepting native ETH.
|
|
374
|
+
JBTerminalConfig[] memory terminalConfigs = _ethTerminalConfig();
|
|
375
|
+
|
|
376
|
+
// Build allowed posts for the deployer.
|
|
377
|
+
CTDeployerAllowedPost[] memory allowedPosts = new CTDeployerAllowedPost[](1);
|
|
378
|
+
allowedPosts[0] = CTDeployerAllowedPost({
|
|
379
|
+
category: POST_CATEGORY,
|
|
380
|
+
minimumPrice: 0,
|
|
381
|
+
minimumTotalSupply: 1,
|
|
382
|
+
maximumTotalSupply: 10_000,
|
|
383
|
+
maximumSplitPercent: 500_000_000, // 50%
|
|
384
|
+
allowedAddresses: new address[](0) // anyone can post
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
CTProjectConfig memory config = CTProjectConfig({
|
|
388
|
+
terminalConfigurations: terminalConfigs,
|
|
389
|
+
projectUri: "https://test.croptop.eth/",
|
|
390
|
+
allowedPosts: allowedPosts,
|
|
391
|
+
contractUri: "https://test.croptop.eth/contract",
|
|
392
|
+
name: "TestCrop",
|
|
393
|
+
symbol: "TCROP",
|
|
394
|
+
salt: bytes32(uint256(1))
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
CTSuckerDeploymentConfig memory suckerConfig =
|
|
398
|
+
CTSuckerDeploymentConfig({deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: bytes32(0)});
|
|
399
|
+
|
|
400
|
+
(projectId, hook) = deployer.deployProjectFor(projectOwner, config, suckerConfig, jbController);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/// @notice Build a JBTerminalConfig[] with a single entry for native ETH.
|
|
404
|
+
function _ethTerminalConfig() internal view returns (JBTerminalConfig[] memory configs) {
|
|
405
|
+
JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
|
|
406
|
+
contexts[0] = JBAccountingContext({
|
|
407
|
+
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
configs = new JBTerminalConfig[](1);
|
|
411
|
+
configs[0] =
|
|
412
|
+
JBTerminalConfig({terminal: IJBTerminal(address(jbMultiTerminal)), accountingContextsToAccept: contexts});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/// @notice Build a single-element CTPost array.
|
|
416
|
+
function _singlePost(
|
|
417
|
+
// forge-lint: disable-next-line(mixed-case-variable)
|
|
418
|
+
bytes32 encodedIPFSUri,
|
|
419
|
+
uint104 price,
|
|
420
|
+
uint32 totalSupply,
|
|
421
|
+
uint24 category
|
|
422
|
+
)
|
|
423
|
+
internal
|
|
424
|
+
pure
|
|
425
|
+
returns (CTPost[] memory posts)
|
|
426
|
+
{
|
|
427
|
+
posts = new CTPost[](1);
|
|
428
|
+
posts[0] = CTPost({
|
|
429
|
+
encodedIPFSUri: encodedIPFSUri,
|
|
430
|
+
price: price,
|
|
431
|
+
totalSupply: totalSupply,
|
|
432
|
+
category: category,
|
|
433
|
+
splitPercent: 0,
|
|
434
|
+
splits: new JBSplit[](0)
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
}
|