@bananapus/721-hook-v6 0.0.42 → 0.0.45

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.
Files changed (86) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +20 -9
  4. package/script/Deploy.s.sol +2 -2
  5. package/src/JB721Checkpoints.sol +61 -19
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +66 -53
  8. package/src/JB721TiersHookDeployer.sol +8 -5
  9. package/src/JB721TiersHookProjectDeployer.sol +87 -46
  10. package/src/JB721TiersHookStore.sol +137 -107
  11. package/src/abstract/JB721Hook.sol +8 -6
  12. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  13. package/src/interfaces/IJB721CheckpointsDeployer.sol +7 -3
  14. package/src/interfaces/IJB721TiersHook.sol +3 -3
  15. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +4 -2
  16. package/src/interfaces/IJB721TiersHookStore.sol +11 -11
  17. package/src/libraries/JB721TiersHookLib.sol +1 -1
  18. package/src/structs/JB721TiersHookFlags.sol +1 -1
  19. package/src/structs/JBPayDataHookRulesetMetadata.sol +1 -1
  20. package/test/utils/AccessJBLib.sol +49 -0
  21. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  22. package/test/utils/TestBaseWorkflow.sol +213 -0
  23. package/test/utils/UnitTestSetup.sol +805 -0
  24. package/.gas-snapshot +0 -152
  25. package/ADMINISTRATION.md +0 -87
  26. package/ARCHITECTURE.md +0 -98
  27. package/AUDIT_INSTRUCTIONS.md +0 -77
  28. package/RISKS.md +0 -118
  29. package/SKILLS.md +0 -43
  30. package/STYLE_GUIDE.md +0 -610
  31. package/USER_JOURNEYS.md +0 -121
  32. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  33. package/slither-ci.config.json +0 -10
  34. package/test/721HookAttacks.t.sol +0 -408
  35. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  36. package/test/Fork.t.sol +0 -2346
  37. package/test/TestAuditGaps.sol +0 -1075
  38. package/test/TestCheckpoints.t.sol +0 -341
  39. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  40. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  41. package/test/audit/AuditRegressions.t.sol +0 -83
  42. package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
  43. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  44. package/test/audit/FreshAudit.t.sol +0 -197
  45. package/test/audit/FutureTierPoC.t.sol +0 -39
  46. package/test/audit/FutureTierRemoval.t.sol +0 -47
  47. package/test/audit/Pass12L18.t.sol +0 -80
  48. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  49. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  50. package/test/audit/RepoFindings.t.sol +0 -195
  51. package/test/audit/ReserveActivation.t.sol +0 -87
  52. package/test/audit/ReserveSlotProtection.t.sol +0 -273
  53. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  54. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  55. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  56. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  57. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  58. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  59. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  60. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  61. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  62. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  63. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  64. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  65. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  66. package/test/regression/CacheTierLookup.t.sol +0 -190
  67. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  68. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  69. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  70. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  71. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  72. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  73. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  74. package/test/unit/JBBitmap.t.sol +0 -170
  75. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  76. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  77. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  78. package/test/unit/deployer_Unit.t.sol +0 -114
  79. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  80. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  81. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  82. package/test/unit/pay_Unit.t.sol +0 -1661
  83. package/test/unit/redeem_Unit.t.sol +0 -473
  84. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  85. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  86. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
@@ -1,596 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "forge-std/Test.sol";
6
-
7
- // forge-lint: disable-next-line(unaliased-plain-import)
8
- import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
9
- // forge-lint: disable-next-line(unaliased-plain-import)
10
- import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
- // forge-lint: disable-next-line(unaliased-plain-import)
12
- import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
13
-
14
- // forge-lint: disable-next-line(unaliased-plain-import)
15
- import "@bananapus/core-v6/src/JBController.sol";
16
- // forge-lint: disable-next-line(unaliased-plain-import)
17
- import "@bananapus/core-v6/src/JBDirectory.sol";
18
- // forge-lint: disable-next-line(unaliased-plain-import)
19
- import "@bananapus/core-v6/src/JBMultiTerminal.sol";
20
- // forge-lint: disable-next-line(unaliased-plain-import)
21
- import "@bananapus/core-v6/src/JBFundAccessLimits.sol";
22
- // forge-lint: disable-next-line(unaliased-plain-import)
23
- import "@bananapus/core-v6/src/JBFeelessAddresses.sol";
24
- // forge-lint: disable-next-line(unaliased-plain-import)
25
- import "@bananapus/core-v6/src/JBTerminalStore.sol";
26
- // forge-lint: disable-next-line(unaliased-plain-import)
27
- import "@bananapus/core-v6/src/JBRulesets.sol";
28
- // forge-lint: disable-next-line(unaliased-plain-import)
29
- import "@bananapus/core-v6/src/JBPermissions.sol";
30
- // forge-lint: disable-next-line(unaliased-plain-import)
31
- import "@bananapus/core-v6/src/JBPrices.sol";
32
- import {JBProjects} from "@bananapus/core-v6/src/JBProjects.sol";
33
- // forge-lint: disable-next-line(unaliased-plain-import)
34
- import "@bananapus/core-v6/src/JBSplits.sol";
35
- // forge-lint: disable-next-line(unaliased-plain-import)
36
- import "@bananapus/core-v6/src/JBERC20.sol";
37
- // forge-lint: disable-next-line(unaliased-plain-import)
38
- import "@bananapus/core-v6/src/JBTokens.sol";
39
- // forge-lint: disable-next-line(unaliased-plain-import)
40
- import "@bananapus/core-v6/src/libraries/JBConstants.sol";
41
- // forge-lint: disable-next-line(unaliased-plain-import)
42
- import "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
43
- // forge-lint: disable-next-line(unaliased-plain-import)
44
- import "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
45
- // forge-lint: disable-next-line(unaliased-plain-import)
46
- import "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
47
- // forge-lint: disable-next-line(unaliased-plain-import)
48
- import "@bananapus/core-v6/src/structs/JBSplit.sol";
49
- // forge-lint: disable-next-line(unaliased-plain-import)
50
- import "@bananapus/core-v6/src/structs/JBFundAccessLimitGroup.sol";
51
- // forge-lint: disable-next-line(unaliased-plain-import)
52
- import "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
53
- // forge-lint: disable-next-line(unaliased-plain-import)
54
- import "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
55
- // forge-lint: disable-next-line(unaliased-plain-import)
56
- import "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
57
- // forge-lint: disable-next-line(unaliased-plain-import)
58
- import "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
59
- import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
60
- import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
61
-
62
- // forge-lint: disable-next-line(unaliased-plain-import)
63
- import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
64
-
65
- // forge-lint: disable-next-line(unaliased-plain-import)
66
- import "../../src/JB721TiersHook.sol";
67
- // forge-lint: disable-next-line(unaliased-plain-import)
68
- import "../../src/JB721TiersHookDeployer.sol";
69
- // forge-lint: disable-next-line(unaliased-plain-import)
70
- import "../../src/JB721TiersHookProjectDeployer.sol";
71
- // forge-lint: disable-next-line(unaliased-plain-import)
72
- import "../../src/JB721TiersHookStore.sol";
73
- import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
74
- import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
75
- // forge-lint: disable-next-line(unaliased-plain-import)
76
- import "../../src/interfaces/IJB721TiersHook.sol";
77
- // forge-lint: disable-next-line(unaliased-plain-import)
78
- import "../../src/structs/JBDeploy721TiersHookConfig.sol";
79
- // forge-lint: disable-next-line(unaliased-plain-import)
80
- import "../../src/structs/JBLaunchProjectConfig.sol";
81
- // forge-lint: disable-next-line(unaliased-plain-import)
82
- import "../../src/structs/JBPayDataHookRulesetConfig.sol";
83
- // forge-lint: disable-next-line(unaliased-plain-import)
84
- import "../../src/structs/JBPayDataHookRulesetMetadata.sol";
85
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
86
-
87
- /// @notice Mock ERC20 with 6 decimals (USDC-like).
88
- contract MockUSDC6 is ERC20 {
89
- constructor() ERC20("Mock USDC", "USDC") {}
90
-
91
- function decimals() public pure override returns (uint8) {
92
- return 6;
93
- }
94
-
95
- function mint(address to, uint256 amount) external {
96
- _mint(to, amount);
97
- }
98
- }
99
-
100
- /// @title ERC20TierSplitFork
101
- /// @notice Fork tests for ERC20 tier split distribution in JB721TiersHook.
102
- /// @dev Run with: forge test --match-contract ERC20TierSplitFork -vvv --fork-url $RPC
103
- contract ERC20TierSplitFork is Test {
104
- using JBRulesetMetadataResolver for JBRuleset;
105
-
106
- // Actors
107
- address multisig = address(0xBEEF);
108
- address payer = makeAddr("payer");
109
- address beneficiary = makeAddr("beneficiary");
110
- address splitBeneficiary = makeAddr("splitBeneficiary");
111
- address reserveBeneficiary = makeAddr("reserveBeneficiary");
112
-
113
- // JB Core
114
- JBPermissions jbPermissions;
115
- JBProjects jbProjects;
116
- JBDirectory jbDirectory;
117
- JBRulesets jbRulesets;
118
- JBTokens jbTokens;
119
- JBSplits jbSplits;
120
- JBFundAccessLimits jbFundAccessLimits;
121
- JBFeelessAddresses jbFeelessAddresses;
122
- JBPrices jbPrices;
123
- JBController jbController;
124
- JBTerminalStore jbTerminalStore;
125
- JBMultiTerminal jbMultiTerminal;
126
-
127
- // 721 Hook
128
- JB721TiersHookStore store;
129
- JB721TiersHook hookImpl;
130
- JB721TiersHookDeployer hookDeployer;
131
- JB721TiersHookProjectDeployer projectDeployer;
132
- MetadataResolverHelper metadataHelper;
133
- JBAddressRegistry addressRegistry;
134
-
135
- // Token
136
- MockUSDC6 usdc;
137
-
138
- receive() external payable {}
139
-
140
- function setUp() public {
141
- vm.createSelectFork("ethereum");
142
-
143
- _deployJBCore();
144
- _deploy721Hook();
145
-
146
- usdc = new MockUSDC6();
147
- usdc.mint(payer, 100_000e6);
148
-
149
- vm.deal(payer, 10 ether);
150
- vm.deal(multisig, 10 ether);
151
- }
152
-
153
- // forge-lint: disable-next-line(mixed-case-function)
154
- function _deployJBCore() internal {
155
- jbPermissions = new JBPermissions(address(0));
156
- jbProjects = new JBProjects(multisig, address(0), address(0));
157
- jbDirectory = new JBDirectory(jbPermissions, jbProjects, multisig);
158
- JBERC20 jbErc20 = new JBERC20(jbPermissions, jbProjects);
159
- jbTokens = new JBTokens(jbDirectory, jbErc20);
160
- jbRulesets = new JBRulesets(jbDirectory);
161
- jbPrices = new JBPrices(jbDirectory, jbPermissions, jbProjects, multisig, address(0));
162
- jbSplits = new JBSplits(jbDirectory);
163
- jbFundAccessLimits = new JBFundAccessLimits(jbDirectory);
164
- jbFeelessAddresses = new JBFeelessAddresses(multisig);
165
-
166
- jbController = new JBController(
167
- jbDirectory,
168
- jbFundAccessLimits,
169
- jbPermissions,
170
- jbPrices,
171
- jbProjects,
172
- jbRulesets,
173
- jbSplits,
174
- jbTokens,
175
- address(0),
176
- address(0)
177
- );
178
-
179
- vm.prank(multisig);
180
- jbDirectory.setIsAllowedToSetFirstController(address(jbController), true);
181
-
182
- jbTerminalStore = new JBTerminalStore(jbDirectory, jbPrices, jbRulesets);
183
-
184
- jbMultiTerminal = new JBMultiTerminal(
185
- jbFeelessAddresses,
186
- jbPermissions,
187
- jbProjects,
188
- jbSplits,
189
- jbTerminalStore,
190
- jbTokens,
191
- IPermit2(address(0)),
192
- address(0)
193
- );
194
- }
195
-
196
- function _deploy721Hook() internal {
197
- store = new JB721TiersHookStore();
198
- hookImpl = new JB721TiersHook(
199
- jbDirectory,
200
- jbPermissions,
201
- jbPrices,
202
- jbRulesets,
203
- store,
204
- IJBSplits(address(jbSplits)),
205
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
206
- address(0)
207
- );
208
- addressRegistry = new JBAddressRegistry();
209
- hookDeployer = new JB721TiersHookDeployer(hookImpl, store, addressRegistry, address(0));
210
- projectDeployer = new JB721TiersHookProjectDeployer(
211
- IJBDirectory(jbDirectory), IJBPermissions(jbPermissions), hookDeployer, address(0)
212
- );
213
- metadataHelper = new MetadataResolverHelper();
214
- }
215
-
216
- // =========================================================================
217
- // Launch Helper
218
- // =========================================================================
219
-
220
- // forge-lint: disable-next-line(mixed-case-function)
221
- function _launchERC20Project(
222
- JB721TierConfig[] memory tierConfigs,
223
- address token,
224
- uint8 tokenDecimals
225
- )
226
- internal
227
- returns (uint256 projectId, address dataHook)
228
- {
229
- // forge-lint: disable-next-line(unsafe-typecast)
230
- uint32 currency = uint32(uint160(token));
231
-
232
- JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig({
233
- name: "TestNFT",
234
- symbol: "TNFT",
235
- baseUri: "ipfs://base/",
236
- tokenUriResolver: IJB721TokenUriResolver(address(0)),
237
- contractUri: "ipfs://contract",
238
- tiersConfig: JB721InitTiersConfig({tiers: tierConfigs, currency: currency, decimals: tokenDecimals}),
239
- flags: JB721TiersHookFlags({
240
- preventOverspending: false,
241
- issueTokensForSplits: false,
242
- noNewTiersWithReserves: false,
243
- noNewTiersWithVotes: false,
244
- noNewTiersWithOwnerMinting: false
245
- })
246
- });
247
-
248
- JBPayDataHookRulesetMetadata memory rulesetMetadata = JBPayDataHookRulesetMetadata({
249
- reservedPercent: 0,
250
- cashOutTaxRate: 0,
251
- baseCurrency: currency,
252
- pausePay: false,
253
- pauseCreditTransfers: false,
254
- allowOwnerMinting: true,
255
- allowSetCustomToken: false,
256
- allowTerminalMigration: false,
257
- allowSetTerminals: false,
258
- allowSetController: false,
259
- allowAddAccountingContext: false,
260
- allowAddPriceFeed: false,
261
- ownerMustSendPayouts: false,
262
- holdFees: false,
263
- useTotalSurplusForCashOuts: false,
264
- useDataHookForCashOut: false,
265
- metadata: 0x00
266
- });
267
-
268
- JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
269
- rulesetConfigs[0].mustStartAtOrAfter = 0;
270
- rulesetConfigs[0].duration = 0;
271
- rulesetConfigs[0].weight = 1_000_000e18;
272
- rulesetConfigs[0].weightCutPercent = 0;
273
- rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
274
- rulesetConfigs[0].metadata = rulesetMetadata;
275
-
276
- JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
277
- accountingContexts[0] = JBAccountingContext({token: token, currency: currency, decimals: tokenDecimals});
278
-
279
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
280
- terminalConfigs[0] =
281
- JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContexts});
282
-
283
- JBLaunchProjectConfig memory launchConfig = JBLaunchProjectConfig({
284
- projectUri: "test-erc20-project",
285
- rulesetConfigurations: rulesetConfigs,
286
- terminalConfigurations: terminalConfigs,
287
- memo: ""
288
- });
289
-
290
- IJB721TiersHook hookInstance;
291
- (projectId, hookInstance) =
292
- projectDeployer.launchProjectFor(multisig, hookConfig, launchConfig, jbController, bytes32(0));
293
-
294
- dataHook = address(hookInstance);
295
- }
296
-
297
- /// @dev Launch with ETH accounting (for regression test).
298
- // forge-lint: disable-next-line(mixed-case-function)
299
- function _launchETHProject(JB721TierConfig[] memory tierConfigs)
300
- internal
301
- returns (uint256 projectId, address dataHook)
302
- {
303
- JBDeploy721TiersHookConfig memory hookConfig = JBDeploy721TiersHookConfig({
304
- name: "TestNFT",
305
- symbol: "TNFT",
306
- baseUri: "ipfs://base/",
307
- tokenUriResolver: IJB721TokenUriResolver(address(0)),
308
- contractUri: "ipfs://contract",
309
- tiersConfig: JB721InitTiersConfig({
310
- tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
311
- }),
312
- flags: JB721TiersHookFlags({
313
- preventOverspending: false,
314
- issueTokensForSplits: false,
315
- noNewTiersWithReserves: false,
316
- noNewTiersWithVotes: false,
317
- noNewTiersWithOwnerMinting: false
318
- })
319
- });
320
-
321
- JBPayDataHookRulesetMetadata memory rulesetMetadata = JBPayDataHookRulesetMetadata({
322
- reservedPercent: 0,
323
- cashOutTaxRate: 0,
324
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
325
- pausePay: false,
326
- pauseCreditTransfers: false,
327
- allowOwnerMinting: true,
328
- allowSetCustomToken: false,
329
- allowTerminalMigration: false,
330
- allowSetTerminals: false,
331
- allowSetController: false,
332
- allowAddAccountingContext: false,
333
- allowAddPriceFeed: false,
334
- ownerMustSendPayouts: false,
335
- holdFees: false,
336
- useTotalSurplusForCashOuts: false,
337
- useDataHookForCashOut: false,
338
- metadata: 0x00
339
- });
340
-
341
- JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
342
- rulesetConfigs[0].mustStartAtOrAfter = 0;
343
- rulesetConfigs[0].duration = 0;
344
- rulesetConfigs[0].weight = 1_000_000e18;
345
- rulesetConfigs[0].weightCutPercent = 0;
346
- rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
347
- rulesetConfigs[0].metadata = rulesetMetadata;
348
-
349
- JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
350
- accountingContexts[0] = JBAccountingContext({
351
- token: JBConstants.NATIVE_TOKEN, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
352
- });
353
-
354
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
355
- terminalConfigs[0] =
356
- JBTerminalConfig({terminal: jbMultiTerminal, accountingContextsToAccept: accountingContexts});
357
-
358
- JBLaunchProjectConfig memory launchConfig = JBLaunchProjectConfig({
359
- projectUri: "test-eth-project",
360
- rulesetConfigurations: rulesetConfigs,
361
- terminalConfigurations: terminalConfigs,
362
- memo: ""
363
- });
364
-
365
- IJB721TiersHook hookInstance;
366
- (projectId, hookInstance) =
367
- projectDeployer.launchProjectFor(multisig, hookConfig, launchConfig, jbController, bytes32(0));
368
-
369
- dataHook = address(hookInstance);
370
- }
371
-
372
- // =========================================================================
373
- // Metadata Helper
374
- // =========================================================================
375
-
376
- function _buildPayMetadata(uint16[] memory tierIds, bool allowOverspending) internal view returns (bytes memory) {
377
- bytes[] memory data = new bytes[](1);
378
- data[0] = abi.encode(allowOverspending, tierIds);
379
- bytes4[] memory ids = new bytes4[](1);
380
- ids[0] = JBMetadataResolver.getId("pay", address(hookImpl));
381
- return metadataHelper.createMetadata(ids, data);
382
- }
383
-
384
- function _tokenId(uint256 tierId, uint256 mintNumber) internal pure returns (uint256) {
385
- return tierId * 1_000_000_000 + mintNumber;
386
- }
387
-
388
- // =========================================================================
389
- // Test 1: USDC payment with tier split to beneficiary
390
- // =========================================================================
391
-
392
- function test_fork_usdcPayment_tierSplitToBeneficiary() public {
393
- // Create a tier: 100 USDC, 30% split to splitBeneficiary.
394
- JBSplit[] memory splits = new JBSplit[](1);
395
- splits[0] = JBSplit({
396
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
397
- projectId: 0,
398
- beneficiary: payable(splitBeneficiary),
399
- preferAddToBalance: false,
400
- lockedUntil: 0,
401
- hook: IJBSplitHook(address(0))
402
- });
403
-
404
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
405
- tierConfigs[0] = JB721TierConfig({
406
- price: 100e6,
407
- initialSupply: 100,
408
- votingUnits: 0,
409
- reserveFrequency: 0,
410
- reserveBeneficiary: address(0),
411
- // forge-lint: disable-next-line(unsafe-typecast)
412
- encodedIPFSUri: bytes32("tier1"),
413
- category: 1,
414
- discountPercent: 0,
415
- flags: JB721TierConfigFlags({
416
- allowOwnerMint: false,
417
- useReserveBeneficiaryAsDefault: false,
418
- transfersPausable: false,
419
- useVotingUnits: false,
420
- cantBeRemoved: false,
421
- cantIncreaseDiscountPercent: false,
422
- cantBuyWithCredits: false
423
- }),
424
- splitPercent: 300_000_000, // 30%
425
- splits: splits
426
- });
427
-
428
- (uint256 projectId, address hook) = _launchERC20Project(tierConfigs, address(usdc), 6);
429
-
430
- // Pay 100 USDC to mint tier 1 NFT.
431
- uint16[] memory tierIds = new uint16[](1);
432
- tierIds[0] = 1;
433
- bytes memory meta = _buildPayMetadata(tierIds, true);
434
-
435
- vm.startPrank(payer);
436
- usdc.approve(address(jbMultiTerminal), 100e6);
437
- jbMultiTerminal.pay({
438
- projectId: projectId,
439
- amount: 100e6,
440
- token: address(usdc),
441
- beneficiary: beneficiary,
442
- minReturnedTokens: 0,
443
- memo: "",
444
- metadata: meta
445
- });
446
- vm.stopPrank();
447
-
448
- // Split beneficiary should have received 30% of 100 USDC = 30 USDC.
449
- assertEq(usdc.balanceOf(splitBeneficiary), 30e6, "split beneficiary should have 30 USDC");
450
- // NFT minted to beneficiary.
451
- assertEq(IERC721(hook).balanceOf(beneficiary), 1, "beneficiary should own 1 NFT");
452
- assertEq(IERC721(hook).ownerOf(_tokenId(1, 1)), beneficiary, "beneficiary owns tier 1 NFT");
453
- }
454
-
455
- // =========================================================================
456
- // Test 2: USDC payment with tier split to project
457
- // =========================================================================
458
-
459
- function test_fork_usdcPayment_tierSplitToProject() public {
460
- // First launch a target project that accepts USDC.
461
- JB721TierConfig[] memory emptyTiers = new JB721TierConfig[](0);
462
- (uint256 targetProjectId,) = _launchERC20Project(emptyTiers, address(usdc), 6);
463
-
464
- // Now create the main project with tier split pointing to target project.
465
- JBSplit[] memory splits = new JBSplit[](1);
466
- splits[0] = JBSplit({
467
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
468
- // forge-lint: disable-next-line(unsafe-typecast)
469
- projectId: uint56(targetProjectId),
470
- beneficiary: payable(address(0)),
471
- preferAddToBalance: true,
472
- lockedUntil: 0,
473
- hook: IJBSplitHook(address(0))
474
- });
475
-
476
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
477
- tierConfigs[0] = JB721TierConfig({
478
- price: 100e6,
479
- initialSupply: 100,
480
- votingUnits: 0,
481
- reserveFrequency: 0,
482
- reserveBeneficiary: address(0),
483
- // forge-lint: disable-next-line(unsafe-typecast)
484
- encodedIPFSUri: bytes32("tier1"),
485
- category: 1,
486
- discountPercent: 0,
487
- flags: JB721TierConfigFlags({
488
- allowOwnerMint: false,
489
- useReserveBeneficiaryAsDefault: false,
490
- transfersPausable: false,
491
- useVotingUnits: false,
492
- cantBeRemoved: false,
493
- cantIncreaseDiscountPercent: false,
494
- cantBuyWithCredits: false
495
- }),
496
- splitPercent: 300_000_000, // 30%
497
- splits: splits
498
- });
499
-
500
- (uint256 projectId, address hook) = _launchERC20Project(tierConfigs, address(usdc), 6);
501
-
502
- // Record target project's terminal USDC balance before.
503
- uint256 targetBalanceBefore =
504
- jbTerminalStore.balanceOf(address(jbMultiTerminal), targetProjectId, address(usdc));
505
-
506
- // Pay 100 USDC.
507
- uint16[] memory tierIds = new uint16[](1);
508
- tierIds[0] = 1;
509
- bytes memory meta = _buildPayMetadata(tierIds, true);
510
-
511
- vm.startPrank(payer);
512
- usdc.approve(address(jbMultiTerminal), 100e6);
513
- jbMultiTerminal.pay({
514
- projectId: projectId,
515
- amount: 100e6,
516
- token: address(usdc),
517
- beneficiary: beneficiary,
518
- minReturnedTokens: 0,
519
- memo: "",
520
- metadata: meta
521
- });
522
- vm.stopPrank();
523
-
524
- // Target project should have received 30 USDC via addToBalance.
525
- uint256 targetBalanceAfter = jbTerminalStore.balanceOf(address(jbMultiTerminal), targetProjectId, address(usdc));
526
- assertEq(targetBalanceAfter - targetBalanceBefore, 30e6, "target project should have 30 USDC more");
527
- // NFT minted.
528
- assertEq(IERC721(hook).balanceOf(beneficiary), 1, "beneficiary should own 1 NFT");
529
- }
530
-
531
- // =========================================================================
532
- // Test 3: ETH payment with tier split still works (regression)
533
- // =========================================================================
534
-
535
- function test_fork_ethPayment_tierSplitStillWorks() public {
536
- JBSplit[] memory splits = new JBSplit[](1);
537
- splits[0] = JBSplit({
538
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
539
- projectId: 0,
540
- beneficiary: payable(splitBeneficiary),
541
- preferAddToBalance: false,
542
- lockedUntil: 0,
543
- hook: IJBSplitHook(address(0))
544
- });
545
-
546
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
547
- tierConfigs[0] = JB721TierConfig({
548
- price: 1 ether,
549
- initialSupply: 100,
550
- votingUnits: 0,
551
- reserveFrequency: 0,
552
- reserveBeneficiary: address(0),
553
- // forge-lint: disable-next-line(unsafe-typecast)
554
- encodedIPFSUri: bytes32("tier1"),
555
- category: 1,
556
- discountPercent: 0,
557
- flags: JB721TierConfigFlags({
558
- allowOwnerMint: false,
559
- useReserveBeneficiaryAsDefault: false,
560
- transfersPausable: false,
561
- useVotingUnits: false,
562
- cantBeRemoved: false,
563
- cantIncreaseDiscountPercent: false,
564
- cantBuyWithCredits: false
565
- }),
566
- splitPercent: 500_000_000, // 50%
567
- splits: splits
568
- });
569
-
570
- (uint256 projectId, address hook) = _launchETHProject(tierConfigs);
571
-
572
- uint256 splitBalanceBefore = splitBeneficiary.balance;
573
-
574
- // Pay 1 ETH.
575
- uint16[] memory tierIds = new uint16[](1);
576
- tierIds[0] = 1;
577
- bytes memory meta = _buildPayMetadata(tierIds, true);
578
-
579
- vm.prank(payer);
580
- jbMultiTerminal.pay{value: 1 ether}({
581
- projectId: projectId,
582
- amount: 1 ether,
583
- token: JBConstants.NATIVE_TOKEN,
584
- beneficiary: beneficiary,
585
- minReturnedTokens: 0,
586
- memo: "",
587
- metadata: meta
588
- });
589
-
590
- // Split beneficiary should have received 50% of 1 ETH = 0.5 ETH.
591
- assertEq(splitBeneficiary.balance - splitBalanceBefore, 0.5 ether, "split beneficiary should have 0.5 ETH");
592
- // NFT minted.
593
- assertEq(IERC721(hook).balanceOf(beneficiary), 1, "beneficiary should own 1 NFT");
594
- assertEq(IERC721(hook).ownerOf(_tokenId(1, 1)), beneficiary, "beneficiary owns tier 1 NFT");
595
- }
596
- }