@bananapus/721-hook-v6 0.0.13 → 0.0.15
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/ARCHITECTURE.md +7 -2
- package/RISKS.md +6 -6
- package/SKILLS.md +5 -4
- package/STYLE_GUIDE.md +131 -43
- package/foundry.toml +3 -3
- package/package.json +5 -5
- package/remappings.txt +1 -1
- package/script/Deploy.s.sol +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +1 -1
- package/src/JB721TiersHook.sol +23 -0
- package/src/JB721TiersHookProjectDeployer.sol +3 -3
- package/src/libraries/JB721TiersHookLib.sol +44 -0
- package/src/structs/JBPayDataHookRulesetMetadata.sol +3 -0
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +2 -0
- package/test/Fork.t.sol +14 -18
- package/test/fork/ERC20TierSplitFork.t.sol +539 -0
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -2
- package/test/unit/pay_CrossCurrency_Unit.t.sol +498 -0
- package/test/unit/tierSplitRouting_Unit.t.sol +257 -0
- /package/test/regression/{L35_CacheTierLookup.t.sol → CacheTierLookup.t.sol} +0 -0
- /package/test/regression/{L34_ReserveBeneficiaryOverwrite.t.sol → ReserveBeneficiaryOverwrite.t.sol} +0 -0
- /package/test/regression/{L36_SplitNoBeneficiary.t.sol → SplitNoBeneficiary.t.sol} +0 -0
- /package/test/unit/{M6_TierSupplyCheck.t.sol → TierSupplyReserveCheck.t.sol} +0 -0
package/test/Fork.t.sol
CHANGED
|
@@ -122,12 +122,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
122
122
|
receive() external payable {}
|
|
123
123
|
|
|
124
124
|
function setUp() public {
|
|
125
|
-
|
|
126
|
-
if (bytes(rpcUrl).length == 0) {
|
|
127
|
-
vm.skip(true);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
vm.createSelectFork(rpcUrl);
|
|
125
|
+
vm.createSelectFork("ethereum");
|
|
131
126
|
|
|
132
127
|
_deployJBCore();
|
|
133
128
|
_deploy721Hook();
|
|
@@ -244,6 +239,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
244
239
|
pausePay: false,
|
|
245
240
|
pauseCreditTransfers: false,
|
|
246
241
|
allowOwnerMinting: true,
|
|
242
|
+
allowSetCustomToken: false,
|
|
247
243
|
allowTerminalMigration: false,
|
|
248
244
|
allowSetTerminals: false,
|
|
249
245
|
allowSetController: false,
|
|
@@ -345,7 +341,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
345
341
|
|
|
346
342
|
/// @dev Build pay metadata that requests specific tier IDs. `allowOverspending` controls revert behavior.
|
|
347
343
|
function _buildPayMetadata(
|
|
348
|
-
address
|
|
344
|
+
address,
|
|
349
345
|
uint16[] memory tierIds,
|
|
350
346
|
bool allowOverspending
|
|
351
347
|
)
|
|
@@ -361,7 +357,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
361
357
|
}
|
|
362
358
|
|
|
363
359
|
/// @dev Build cash out metadata that specifies token IDs to burn.
|
|
364
|
-
function _buildCashOutMetadata(address
|
|
360
|
+
function _buildCashOutMetadata(address, uint256[] memory tokenIds) internal view returns (bytes memory) {
|
|
365
361
|
bytes[] memory data = new bytes[](1);
|
|
366
362
|
data[0] = abi.encode(tokenIds);
|
|
367
363
|
bytes4[] memory ids = new bytes4[](1);
|
|
@@ -734,7 +730,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
734
730
|
tierConfigs[0].reserveFrequency = 0; // Initial tier has no reserves (allowed).
|
|
735
731
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
736
732
|
flags.noNewTiersWithReserves = true;
|
|
737
|
-
(
|
|
733
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
738
734
|
|
|
739
735
|
// Try to add a new tier with reserves.
|
|
740
736
|
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
@@ -767,7 +763,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
767
763
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, false);
|
|
768
764
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
769
765
|
flags.noNewTiersWithOwnerMinting = true;
|
|
770
|
-
(
|
|
766
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
771
767
|
|
|
772
768
|
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
773
769
|
newTiers[0] = JB721TierConfig({
|
|
@@ -799,7 +795,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
799
795
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, false);
|
|
800
796
|
tierConfigs[0].cannotBeRemoved = true;
|
|
801
797
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
802
|
-
(
|
|
798
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
803
799
|
|
|
804
800
|
uint256[] memory toRemove = new uint256[](1);
|
|
805
801
|
toRemove[0] = 1;
|
|
@@ -864,7 +860,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
864
860
|
tierConfigs[0].discountPercent = 50;
|
|
865
861
|
tierConfigs[0].cannotIncreaseDiscountPercent = true;
|
|
866
862
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
867
|
-
(
|
|
863
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
868
864
|
|
|
869
865
|
vm.prank(multisig);
|
|
870
866
|
vm.expectRevert();
|
|
@@ -1037,7 +1033,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1037
1033
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, true); // allowOwnerMint=true
|
|
1038
1034
|
tierConfigs[0].reserveFrequency = 0; // No reserves (required: can't have both).
|
|
1039
1035
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
1040
|
-
(
|
|
1036
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1041
1037
|
|
|
1042
1038
|
uint16[] memory tierIds = new uint16[](2);
|
|
1043
1039
|
tierIds[0] = 1;
|
|
@@ -1054,7 +1050,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1054
1050
|
JB721TierConfig[] memory tierConfigs = _makeStandardTiers(1, 10, true);
|
|
1055
1051
|
tierConfigs[0].reserveFrequency = 0;
|
|
1056
1052
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
1057
|
-
(
|
|
1053
|
+
(, address hook) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1058
1054
|
|
|
1059
1055
|
uint16[] memory tierIds = new uint16[](1);
|
|
1060
1056
|
tierIds[0] = 1;
|
|
@@ -1408,7 +1404,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1408
1404
|
|
|
1409
1405
|
/// @notice Calling initialize() again on a deployed hook should revert.
|
|
1410
1406
|
function test_fork_reInitialize_reverts() public {
|
|
1411
|
-
(
|
|
1407
|
+
(, address hook,) = _launchStandardProject();
|
|
1412
1408
|
|
|
1413
1409
|
JB721TierConfig[] memory emptyTiers = new JB721TierConfig[](0);
|
|
1414
1410
|
|
|
@@ -1468,7 +1464,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1468
1464
|
|
|
1469
1465
|
/// @notice Non-owner cannot adjustTiers.
|
|
1470
1466
|
function test_fork_adjustTiers_noPermission_reverts() public {
|
|
1471
|
-
(
|
|
1467
|
+
(, address hook,) = _launchStandardProject();
|
|
1472
1468
|
|
|
1473
1469
|
JB721TierConfig[] memory newTiers = new JB721TierConfig[](1);
|
|
1474
1470
|
newTiers[0] = JB721TierConfig({
|
|
@@ -1906,7 +1902,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1906
1902
|
JB721TiersHookFlags memory flags = _defaultFlags();
|
|
1907
1903
|
|
|
1908
1904
|
(uint256 projectId1, address hook1) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1909
|
-
(
|
|
1905
|
+
(, address hook2) = _launchProject(tierConfigs, flags, 5000, true, 0x00);
|
|
1910
1906
|
|
|
1911
1907
|
assertTrue(hook1 != hook2, "hooks are different clones");
|
|
1912
1908
|
|
|
@@ -1989,7 +1985,7 @@ contract Fork_721Hook_Test is Test {
|
|
|
1989
1985
|
uint24 category
|
|
1990
1986
|
)
|
|
1991
1987
|
internal
|
|
1992
|
-
|
|
1988
|
+
pure
|
|
1993
1989
|
returns (JB721TierConfig memory)
|
|
1994
1990
|
{
|
|
1995
1991
|
return JB721TierConfig({
|
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity 0.8.26;
|
|
3
|
+
|
|
4
|
+
import "forge-std/Test.sol";
|
|
5
|
+
|
|
6
|
+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
7
|
+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
+
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
|
|
9
|
+
|
|
10
|
+
import "@bananapus/core-v6/src/JBController.sol";
|
|
11
|
+
import "@bananapus/core-v6/src/JBDirectory.sol";
|
|
12
|
+
import "@bananapus/core-v6/src/JBMultiTerminal.sol";
|
|
13
|
+
import "@bananapus/core-v6/src/JBFundAccessLimits.sol";
|
|
14
|
+
import "@bananapus/core-v6/src/JBFeelessAddresses.sol";
|
|
15
|
+
import "@bananapus/core-v6/src/JBTerminalStore.sol";
|
|
16
|
+
import "@bananapus/core-v6/src/JBRulesets.sol";
|
|
17
|
+
import "@bananapus/core-v6/src/JBPermissions.sol";
|
|
18
|
+
import "@bananapus/core-v6/src/JBPrices.sol";
|
|
19
|
+
import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
|
|
20
|
+
import "@bananapus/core-v6/src/JBSplits.sol";
|
|
21
|
+
import "@bananapus/core-v6/src/JBERC20.sol";
|
|
22
|
+
import "@bananapus/core-v6/src/JBTokens.sol";
|
|
23
|
+
import "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
24
|
+
import "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
25
|
+
import "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
26
|
+
import "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
27
|
+
import "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
28
|
+
import "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
|
|
29
|
+
import "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
|
|
30
|
+
import "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
31
|
+
import "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
32
|
+
import "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
33
|
+
import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
|
|
34
|
+
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
35
|
+
|
|
36
|
+
import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
|
|
37
|
+
|
|
38
|
+
import "../../src/JB721TiersHook.sol";
|
|
39
|
+
import "../../src/JB721TiersHookDeployer.sol";
|
|
40
|
+
import "../../src/JB721TiersHookProjectDeployer.sol";
|
|
41
|
+
import "../../src/JB721TiersHookStore.sol";
|
|
42
|
+
import "../../src/interfaces/IJB721TiersHook.sol";
|
|
43
|
+
import "../../src/structs/JBDeploy721TiersHookConfig.sol";
|
|
44
|
+
import "../../src/structs/JBLaunchProjectConfig.sol";
|
|
45
|
+
import "../../src/structs/JBPayDataHookRulesetConfig.sol";
|
|
46
|
+
import "../../src/structs/JBPayDataHookRulesetMetadata.sol";
|
|
47
|
+
|
|
48
|
+
/// @notice Mock ERC20 with 6 decimals (USDC-like).
|
|
49
|
+
contract MockUSDC6 is ERC20 {
|
|
50
|
+
constructor() ERC20("Mock USDC", "USDC") {}
|
|
51
|
+
|
|
52
|
+
function decimals() public pure override returns (uint8) {
|
|
53
|
+
return 6;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mint(address to, uint256 amount) external {
|
|
57
|
+
_mint(to, amount);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// @title ERC20TierSplitFork
|
|
62
|
+
/// @notice Fork tests for ERC20 tier split distribution in JB721TiersHook.
|
|
63
|
+
/// @dev Run with: forge test --match-contract ERC20TierSplitFork -vvv --fork-url $RPC
|
|
64
|
+
contract ERC20TierSplitFork is Test {
|
|
65
|
+
using JBRulesetMetadataResolver for JBRuleset;
|
|
66
|
+
|
|
67
|
+
// Actors
|
|
68
|
+
address multisig = address(0xBEEF);
|
|
69
|
+
address payer = makeAddr("payer");
|
|
70
|
+
address beneficiary = makeAddr("beneficiary");
|
|
71
|
+
address splitBeneficiary = makeAddr("splitBeneficiary");
|
|
72
|
+
address reserveBeneficiary = makeAddr("reserveBeneficiary");
|
|
73
|
+
|
|
74
|
+
// JB Core
|
|
75
|
+
JBPermissions jbPermissions;
|
|
76
|
+
JBProjects jbProjects;
|
|
77
|
+
JBDirectory jbDirectory;
|
|
78
|
+
JBRulesets jbRulesets;
|
|
79
|
+
JBTokens jbTokens;
|
|
80
|
+
JBSplits jbSplits;
|
|
81
|
+
JBFundAccessLimits jbFundAccessLimits;
|
|
82
|
+
JBFeelessAddresses jbFeelessAddresses;
|
|
83
|
+
JBPrices jbPrices;
|
|
84
|
+
JBController jbController;
|
|
85
|
+
JBTerminalStore jbTerminalStore;
|
|
86
|
+
JBMultiTerminal jbMultiTerminal;
|
|
87
|
+
|
|
88
|
+
// 721 Hook
|
|
89
|
+
JB721TiersHookStore store;
|
|
90
|
+
JB721TiersHook hookImpl;
|
|
91
|
+
JB721TiersHookDeployer hookDeployer;
|
|
92
|
+
JB721TiersHookProjectDeployer projectDeployer;
|
|
93
|
+
MetadataResolverHelper metadataHelper;
|
|
94
|
+
JBAddressRegistry addressRegistry;
|
|
95
|
+
|
|
96
|
+
// Token
|
|
97
|
+
MockUSDC6 usdc;
|
|
98
|
+
|
|
99
|
+
receive() external payable {}
|
|
100
|
+
|
|
101
|
+
function setUp() public {
|
|
102
|
+
vm.createSelectFork("ethereum");
|
|
103
|
+
|
|
104
|
+
_deployJBCore();
|
|
105
|
+
_deploy721Hook();
|
|
106
|
+
|
|
107
|
+
usdc = new MockUSDC6();
|
|
108
|
+
usdc.mint(payer, 100_000e6);
|
|
109
|
+
|
|
110
|
+
vm.deal(payer, 10 ether);
|
|
111
|
+
vm.deal(multisig, 10 ether);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _deployJBCore() internal {
|
|
115
|
+
jbPermissions = new JBPermissions(address(0));
|
|
116
|
+
jbProjects = new JBProjects(multisig, address(0), address(0));
|
|
117
|
+
jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
|
|
118
|
+
JBERC20 jbErc20 = new JBERC20();
|
|
119
|
+
jbTokens = new JBTokens(jbDirectory, jbErc20);
|
|
120
|
+
jbRulesets = new JBRulesets(jbDirectory);
|
|
121
|
+
jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, address(0));
|
|
122
|
+
jbSplits = new JBSplits(jbDirectory);
|
|
123
|
+
jbFundAccessLimits = new JBFundAccessLimits(jbDirectory);
|
|
124
|
+
jbFeelessAddresses = new JBFeelessAddresses(multisig);
|
|
125
|
+
|
|
126
|
+
jbController = new JBController(
|
|
127
|
+
jbDirectory,
|
|
128
|
+
jbFundAccessLimits,
|
|
129
|
+
jbPermissions,
|
|
130
|
+
jbPrices,
|
|
131
|
+
jbProjects,
|
|
132
|
+
jbRulesets,
|
|
133
|
+
jbSplits,
|
|
134
|
+
jbTokens,
|
|
135
|
+
address(0),
|
|
136
|
+
address(0)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
vm.prank(multisig);
|
|
140
|
+
jbDirectory.setIsAllowedToSetFirstController(address(jbController), true);
|
|
141
|
+
|
|
142
|
+
jbTerminalStore = new JBTerminalStore(jbDirectory, jbPrices, jbRulesets);
|
|
143
|
+
|
|
144
|
+
jbMultiTerminal = new JBMultiTerminal(
|
|
145
|
+
jbFeelessAddresses,
|
|
146
|
+
jbPermissions,
|
|
147
|
+
jbProjects,
|
|
148
|
+
jbSplits,
|
|
149
|
+
jbTerminalStore,
|
|
150
|
+
jbTokens,
|
|
151
|
+
IPermit2(address(0)),
|
|
152
|
+
address(0)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _deploy721Hook() internal {
|
|
157
|
+
store = new JB721TiersHookStore();
|
|
158
|
+
hookImpl =
|
|
159
|
+
new JB721TiersHook(jbDirectory, jbPermissions, jbRulesets, store, IJBSplits(address(jbSplits)), address(0));
|
|
160
|
+
addressRegistry = new JBAddressRegistry();
|
|
161
|
+
hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, address(0));
|
|
162
|
+
projectDeployer = new JB721TiersHookProjectDeployer(
|
|
163
|
+
IJBDirectory(jbDirectory), IJBPermissions(jbPermissions), hookDeployer, address(0)
|
|
164
|
+
);
|
|
165
|
+
metadataHelper = new MetadataResolverHelper();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =========================================================================
|
|
169
|
+
// Launch Helper
|
|
170
|
+
// =========================================================================
|
|
171
|
+
|
|
172
|
+
function _launchERC20Project(
|
|
173
|
+
JB721TierConfig[] memory tierConfigs,
|
|
174
|
+
address token,
|
|
175
|
+
uint8 tokenDecimals
|
|
176
|
+
)
|
|
177
|
+
internal
|
|
178
|
+
returns (uint256 projectId, address dataHook)
|
|
179
|
+
{
|
|
180
|
+
uint32 currency = uint32(uint160(token));
|
|
181
|
+
|
|
182
|
+
JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig({
|
|
183
|
+
name: "TestNFT",
|
|
184
|
+
symbol: "TNFT",
|
|
185
|
+
baseUri: "ipfs://base/",
|
|
186
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
187
|
+
contractUri: "ipfs://contract",
|
|
188
|
+
tiersConfig: JB721InitTiersConfig({
|
|
189
|
+
tiers: tierConfigs, currency: currency, decimals: tokenDecimals, prices: IJBPrices(address(0))
|
|
190
|
+
}),
|
|
191
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
192
|
+
flags: JB721TiersHookFlags({
|
|
193
|
+
preventOverspending: false,
|
|
194
|
+
issueTokensForSplits: false,
|
|
195
|
+
noNewTiersWithReserves: false,
|
|
196
|
+
noNewTiersWithVotes: false,
|
|
197
|
+
noNewTiersWithOwnerMinting: false
|
|
198
|
+
})
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
JBPayDataHookRulesetMetadata memory rulesetMetadata = JBPayDataHookRulesetMetadata({
|
|
202
|
+
reservedPercent: 0,
|
|
203
|
+
cashOutTaxRate: 0,
|
|
204
|
+
baseCurrency: currency,
|
|
205
|
+
pausePay: false,
|
|
206
|
+
pauseCreditTransfers: false,
|
|
207
|
+
allowOwnerMinting: true,
|
|
208
|
+
allowSetCustomToken: false,
|
|
209
|
+
allowTerminalMigration: false,
|
|
210
|
+
allowSetTerminals: false,
|
|
211
|
+
allowSetController: false,
|
|
212
|
+
allowAddAccountingContext: false,
|
|
213
|
+
allowAddPriceFeed: false,
|
|
214
|
+
ownerMustSendPayouts: false,
|
|
215
|
+
holdFees: false,
|
|
216
|
+
useTotalSurplusForCashOuts: false,
|
|
217
|
+
useDataHookForCashOut: false,
|
|
218
|
+
metadata: 0x00
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
|
|
222
|
+
rulesetConfigs[0].mustStartAtOrAfter = 0;
|
|
223
|
+
rulesetConfigs[0].duration = 0;
|
|
224
|
+
rulesetConfigs[0].weight = 1_000_000e18;
|
|
225
|
+
rulesetConfigs[0].weightCutPercent = 0;
|
|
226
|
+
rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
|
|
227
|
+
rulesetConfigs[0].metadata = rulesetMetadata;
|
|
228
|
+
|
|
229
|
+
JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
|
|
230
|
+
accountingContexts[0] = JBAccountingContext({token: token, currency: currency, decimals: tokenDecimals});
|
|
231
|
+
|
|
232
|
+
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
233
|
+
terminalConfigs[0] =
|
|
234
|
+
JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContexts});
|
|
235
|
+
|
|
236
|
+
JBLaunchProjectConfig memory launchConfig = JBLaunchProjectConfig({
|
|
237
|
+
projectUri: "test-erc20-project",
|
|
238
|
+
rulesetConfigurations: rulesetConfigs,
|
|
239
|
+
terminalConfigurations: terminalConfigs,
|
|
240
|
+
memo: ""
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
IJB721TiersHook hookInstance;
|
|
244
|
+
(projectId, hookInstance) =
|
|
245
|
+
projectDeployer.launchProjectFor(multisig, hookConfig, launchConfig, jbController, bytes32(0));
|
|
246
|
+
|
|
247
|
+
dataHook = address(hookInstance);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// @dev Launch with ETH accounting (for regression test).
|
|
251
|
+
function _launchETHProject(JB721TierConfig[] memory tierConfigs)
|
|
252
|
+
internal
|
|
253
|
+
returns (uint256 projectId, address dataHook)
|
|
254
|
+
{
|
|
255
|
+
JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig({
|
|
256
|
+
name: "TestNFT",
|
|
257
|
+
symbol: "TNFT",
|
|
258
|
+
baseUri: "ipfs://base/",
|
|
259
|
+
tokenUriResolver: IJB721TokenUriResolver(address(0)),
|
|
260
|
+
contractUri: "ipfs://contract",
|
|
261
|
+
tiersConfig: JB721InitTiersConfig({
|
|
262
|
+
tiers: tierConfigs,
|
|
263
|
+
currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
264
|
+
decimals: 18,
|
|
265
|
+
prices: IJBPrices(address(0))
|
|
266
|
+
}),
|
|
267
|
+
reserveBeneficiary: reserveBeneficiary,
|
|
268
|
+
flags: JB721TiersHookFlags({
|
|
269
|
+
preventOverspending: false,
|
|
270
|
+
issueTokensForSplits: false,
|
|
271
|
+
noNewTiersWithReserves: false,
|
|
272
|
+
noNewTiersWithVotes: false,
|
|
273
|
+
noNewTiersWithOwnerMinting: false
|
|
274
|
+
})
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
JBPayDataHookRulesetMetadata memory rulesetMetadata = JBPayDataHookRulesetMetadata({
|
|
278
|
+
reservedPercent: 0,
|
|
279
|
+
cashOutTaxRate: 0,
|
|
280
|
+
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
281
|
+
pausePay: false,
|
|
282
|
+
pauseCreditTransfers: false,
|
|
283
|
+
allowOwnerMinting: true,
|
|
284
|
+
allowSetCustomToken: false,
|
|
285
|
+
allowTerminalMigration: false,
|
|
286
|
+
allowSetTerminals: false,
|
|
287
|
+
allowSetController: false,
|
|
288
|
+
allowAddAccountingContext: false,
|
|
289
|
+
allowAddPriceFeed: false,
|
|
290
|
+
ownerMustSendPayouts: false,
|
|
291
|
+
holdFees: false,
|
|
292
|
+
useTotalSurplusForCashOuts: false,
|
|
293
|
+
useDataHookForCashOut: false,
|
|
294
|
+
metadata: 0x00
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
|
|
298
|
+
rulesetConfigs[0].mustStartAtOrAfter = 0;
|
|
299
|
+
rulesetConfigs[0].duration = 0;
|
|
300
|
+
rulesetConfigs[0].weight = 1_000_000e18;
|
|
301
|
+
rulesetConfigs[0].weightCutPercent = 0;
|
|
302
|
+
rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
|
|
303
|
+
rulesetConfigs[0].metadata = rulesetMetadata;
|
|
304
|
+
|
|
305
|
+
JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
|
|
306
|
+
accountingContexts[0] = JBAccountingContext({
|
|
307
|
+
token: JBConstants.NATIVE_TOKEN, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
|
|
311
|
+
terminalConfigs[0] =
|
|
312
|
+
JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContexts});
|
|
313
|
+
|
|
314
|
+
JBLaunchProjectConfig memory launchConfig = JBLaunchProjectConfig({
|
|
315
|
+
projectUri: "test-eth-project",
|
|
316
|
+
rulesetConfigurations: rulesetConfigs,
|
|
317
|
+
terminalConfigurations: terminalConfigs,
|
|
318
|
+
memo: ""
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
IJB721TiersHook hookInstance;
|
|
322
|
+
(projectId, hookInstance) =
|
|
323
|
+
projectDeployer.launchProjectFor(multisig, hookConfig, launchConfig, jbController, bytes32(0));
|
|
324
|
+
|
|
325
|
+
dataHook = address(hookInstance);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// =========================================================================
|
|
329
|
+
// Metadata Helper
|
|
330
|
+
// =========================================================================
|
|
331
|
+
|
|
332
|
+
function _buildPayMetadata(uint16[] memory tierIds, bool allowOverspending) internal view returns (bytes memory) {
|
|
333
|
+
bytes[] memory data = new bytes[](1);
|
|
334
|
+
data[0] = abi.encode(allowOverspending, tierIds);
|
|
335
|
+
bytes4[] memory ids = new bytes4[](1);
|
|
336
|
+
ids[0] = JBMetadataResolver.getId("pay", address(hookImpl));
|
|
337
|
+
return metadataHelper.createMetadata(ids, data);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function _tokenId(uint256 tierId, uint256 mintNumber) internal pure returns (uint256) {
|
|
341
|
+
return tierId * 1_000_000_000 + mintNumber;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// =========================================================================
|
|
345
|
+
// Test 1: USDC payment with tier split to beneficiary
|
|
346
|
+
// =========================================================================
|
|
347
|
+
|
|
348
|
+
function test_fork_usdcPayment_tierSplitToBeneficiary() public {
|
|
349
|
+
// Create a tier: 100 USDC, 30% split to splitBeneficiary.
|
|
350
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
351
|
+
splits[0] = JBSplit({
|
|
352
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
353
|
+
projectId: 0,
|
|
354
|
+
beneficiary: payable(splitBeneficiary),
|
|
355
|
+
preferAddToBalance: false,
|
|
356
|
+
lockedUntil: 0,
|
|
357
|
+
hook: IJBSplitHook(address(0))
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
361
|
+
tierConfigs[0] = JB721TierConfig({
|
|
362
|
+
price: 100e6,
|
|
363
|
+
initialSupply: 100,
|
|
364
|
+
votingUnits: 0,
|
|
365
|
+
reserveFrequency: 0,
|
|
366
|
+
reserveBeneficiary: address(0),
|
|
367
|
+
encodedIPFSUri: bytes32("tier1"),
|
|
368
|
+
category: 1,
|
|
369
|
+
discountPercent: 0,
|
|
370
|
+
allowOwnerMint: false,
|
|
371
|
+
useReserveBeneficiaryAsDefault: false,
|
|
372
|
+
transfersPausable: false,
|
|
373
|
+
useVotingUnits: false,
|
|
374
|
+
cannotBeRemoved: false,
|
|
375
|
+
cannotIncreaseDiscountPercent: false,
|
|
376
|
+
splitPercent: 300_000_000, // 30%
|
|
377
|
+
splits: splits
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
(uint256 projectId, address hook) = _launchERC20Project(tierConfigs, address(usdc), 6);
|
|
381
|
+
|
|
382
|
+
// Pay 100 USDC to mint tier 1 NFT.
|
|
383
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
384
|
+
tierIds[0] = 1;
|
|
385
|
+
bytes memory meta = _buildPayMetadata(tierIds, true);
|
|
386
|
+
|
|
387
|
+
vm.startPrank(payer);
|
|
388
|
+
usdc.approve(address(jbMultiTerminal), 100e6);
|
|
389
|
+
jbMultiTerminal.pay({
|
|
390
|
+
projectId: projectId,
|
|
391
|
+
amount: 100e6,
|
|
392
|
+
token: address(usdc),
|
|
393
|
+
beneficiary: beneficiary,
|
|
394
|
+
minReturnedTokens: 0,
|
|
395
|
+
memo: "",
|
|
396
|
+
metadata: meta
|
|
397
|
+
});
|
|
398
|
+
vm.stopPrank();
|
|
399
|
+
|
|
400
|
+
// Split beneficiary should have received 30% of 100 USDC = 30 USDC.
|
|
401
|
+
assertEq(usdc.balanceOf(splitBeneficiary), 30e6, "split beneficiary should have 30 USDC");
|
|
402
|
+
// NFT minted to beneficiary.
|
|
403
|
+
assertEq(IERC721(hook).balanceOf(beneficiary), 1, "beneficiary should own 1 NFT");
|
|
404
|
+
assertEq(IERC721(hook).ownerOf(_tokenId(1, 1)), beneficiary, "beneficiary owns tier 1 NFT");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// =========================================================================
|
|
408
|
+
// Test 2: USDC payment with tier split to project
|
|
409
|
+
// =========================================================================
|
|
410
|
+
|
|
411
|
+
function test_fork_usdcPayment_tierSplitToProject() public {
|
|
412
|
+
// First launch a target project that accepts USDC.
|
|
413
|
+
JB721TierConfig[] memory emptyTiers = new JB721TierConfig[](0);
|
|
414
|
+
(uint256 targetProjectId,) = _launchERC20Project(emptyTiers, address(usdc), 6);
|
|
415
|
+
|
|
416
|
+
// Now create the main project with tier split pointing to target project.
|
|
417
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
418
|
+
splits[0] = JBSplit({
|
|
419
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
420
|
+
projectId: uint56(targetProjectId),
|
|
421
|
+
beneficiary: payable(address(0)),
|
|
422
|
+
preferAddToBalance: true,
|
|
423
|
+
lockedUntil: 0,
|
|
424
|
+
hook: IJBSplitHook(address(0))
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
428
|
+
tierConfigs[0] = JB721TierConfig({
|
|
429
|
+
price: 100e6,
|
|
430
|
+
initialSupply: 100,
|
|
431
|
+
votingUnits: 0,
|
|
432
|
+
reserveFrequency: 0,
|
|
433
|
+
reserveBeneficiary: address(0),
|
|
434
|
+
encodedIPFSUri: bytes32("tier1"),
|
|
435
|
+
category: 1,
|
|
436
|
+
discountPercent: 0,
|
|
437
|
+
allowOwnerMint: false,
|
|
438
|
+
useReserveBeneficiaryAsDefault: false,
|
|
439
|
+
transfersPausable: false,
|
|
440
|
+
useVotingUnits: false,
|
|
441
|
+
cannotBeRemoved: false,
|
|
442
|
+
cannotIncreaseDiscountPercent: false,
|
|
443
|
+
splitPercent: 300_000_000, // 30%
|
|
444
|
+
splits: splits
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
(uint256 projectId, address hook) = _launchERC20Project(tierConfigs, address(usdc), 6);
|
|
448
|
+
|
|
449
|
+
// Record target project's terminal USDC balance before.
|
|
450
|
+
uint256 targetBalanceBefore =
|
|
451
|
+
jbTerminalStore.balanceOf(address(jbMultiTerminal), targetProjectId, address(usdc));
|
|
452
|
+
|
|
453
|
+
// Pay 100 USDC.
|
|
454
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
455
|
+
tierIds[0] = 1;
|
|
456
|
+
bytes memory meta = _buildPayMetadata(tierIds, true);
|
|
457
|
+
|
|
458
|
+
vm.startPrank(payer);
|
|
459
|
+
usdc.approve(address(jbMultiTerminal), 100e6);
|
|
460
|
+
jbMultiTerminal.pay({
|
|
461
|
+
projectId: projectId,
|
|
462
|
+
amount: 100e6,
|
|
463
|
+
token: address(usdc),
|
|
464
|
+
beneficiary: beneficiary,
|
|
465
|
+
minReturnedTokens: 0,
|
|
466
|
+
memo: "",
|
|
467
|
+
metadata: meta
|
|
468
|
+
});
|
|
469
|
+
vm.stopPrank();
|
|
470
|
+
|
|
471
|
+
// Target project should have received 30 USDC via addToBalance.
|
|
472
|
+
uint256 targetBalanceAfter = jbTerminalStore.balanceOf(address(jbMultiTerminal), targetProjectId, address(usdc));
|
|
473
|
+
assertEq(targetBalanceAfter - targetBalanceBefore, 30e6, "target project should have 30 USDC more");
|
|
474
|
+
// NFT minted.
|
|
475
|
+
assertEq(IERC721(hook).balanceOf(beneficiary), 1, "beneficiary should own 1 NFT");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// =========================================================================
|
|
479
|
+
// Test 3: ETH payment with tier split still works (regression)
|
|
480
|
+
// =========================================================================
|
|
481
|
+
|
|
482
|
+
function test_fork_ethPayment_tierSplitStillWorks() public {
|
|
483
|
+
JBSplit[] memory splits = new JBSplit[](1);
|
|
484
|
+
splits[0] = JBSplit({
|
|
485
|
+
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
486
|
+
projectId: 0,
|
|
487
|
+
beneficiary: payable(splitBeneficiary),
|
|
488
|
+
preferAddToBalance: false,
|
|
489
|
+
lockedUntil: 0,
|
|
490
|
+
hook: IJBSplitHook(address(0))
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
494
|
+
tierConfigs[0] = JB721TierConfig({
|
|
495
|
+
price: 1 ether,
|
|
496
|
+
initialSupply: 100,
|
|
497
|
+
votingUnits: 0,
|
|
498
|
+
reserveFrequency: 0,
|
|
499
|
+
reserveBeneficiary: address(0),
|
|
500
|
+
encodedIPFSUri: bytes32("tier1"),
|
|
501
|
+
category: 1,
|
|
502
|
+
discountPercent: 0,
|
|
503
|
+
allowOwnerMint: false,
|
|
504
|
+
useReserveBeneficiaryAsDefault: false,
|
|
505
|
+
transfersPausable: false,
|
|
506
|
+
useVotingUnits: false,
|
|
507
|
+
cannotBeRemoved: false,
|
|
508
|
+
cannotIncreaseDiscountPercent: false,
|
|
509
|
+
splitPercent: 500_000_000, // 50%
|
|
510
|
+
splits: splits
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
(uint256 projectId, address hook) = _launchETHProject(tierConfigs);
|
|
514
|
+
|
|
515
|
+
uint256 splitBalanceBefore = splitBeneficiary.balance;
|
|
516
|
+
|
|
517
|
+
// Pay 1 ETH.
|
|
518
|
+
uint16[] memory tierIds = new uint16[](1);
|
|
519
|
+
tierIds[0] = 1;
|
|
520
|
+
bytes memory meta = _buildPayMetadata(tierIds, true);
|
|
521
|
+
|
|
522
|
+
vm.prank(payer);
|
|
523
|
+
jbMultiTerminal.pay{value: 1 ether}({
|
|
524
|
+
projectId: projectId,
|
|
525
|
+
amount: 1 ether,
|
|
526
|
+
token: JBConstants.NATIVE_TOKEN,
|
|
527
|
+
beneficiary: beneficiary,
|
|
528
|
+
minReturnedTokens: 0,
|
|
529
|
+
memo: "",
|
|
530
|
+
metadata: meta
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
// Split beneficiary should have received 50% of 1 ETH = 0.5 ETH.
|
|
534
|
+
assertEq(splitBeneficiary.balance - splitBalanceBefore, 0.5 ether, "split beneficiary should have 0.5 ETH");
|
|
535
|
+
// NFT minted.
|
|
536
|
+
assertEq(IERC721(hook).balanceOf(beneficiary), 1, "beneficiary should own 1 NFT");
|
|
537
|
+
assertEq(IERC721(hook).ownerOf(_tokenId(1, 1)), beneficiary, "beneficiary owns tier 1 NFT");
|
|
538
|
+
}
|
|
539
|
+
}
|
|
@@ -76,7 +76,6 @@ contract TierLifecycleInvariant_Local is StdInvariant, UnitTestSetup {
|
|
|
76
76
|
function invariant_721_2_totalCashOutWeightConsistency() public {
|
|
77
77
|
uint256 totalWeight = store.totalCashOutWeight(address(hook));
|
|
78
78
|
|
|
79
|
-
uint256 maxTierId = store.maxTierIdOf(address(hook));
|
|
80
79
|
uint256 computedWeight = 0;
|
|
81
80
|
|
|
82
81
|
uint256[] memory categories = new uint256[](0);
|
|
@@ -175,7 +174,6 @@ contract TierLifecycleInvariant_Local is StdInvariant, UnitTestSetup {
|
|
|
175
174
|
JB721Tier[] memory allTiers = store.tiersOf(address(hook), categories, false, 0, 100);
|
|
176
175
|
|
|
177
176
|
for (uint256 i = 0; i < allTiers.length; i++) {
|
|
178
|
-
uint256 tierId = allTiers[i].id;
|
|
179
177
|
uint256 price = allTiers[i].price;
|
|
180
178
|
|
|
181
179
|
// Cash out weight per token for this tier is just `price` (from the store)
|