@bananapus/721-hook-v6 0.0.41 → 0.0.43

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 (77) 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 +60 -18
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +4 -1
  8. package/src/JB721TiersHookProjectDeployer.sol +68 -30
  9. package/src/JB721TiersHookStore.sol +1 -4
  10. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  11. package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
  12. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
  13. package/test/utils/AccessJBLib.sol +49 -0
  14. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  15. package/test/utils/TestBaseWorkflow.sol +213 -0
  16. package/test/utils/UnitTestSetup.sol +805 -0
  17. package/.gas-snapshot +0 -152
  18. package/ADMINISTRATION.md +0 -87
  19. package/ARCHITECTURE.md +0 -98
  20. package/AUDIT_INSTRUCTIONS.md +0 -77
  21. package/RISKS.md +0 -118
  22. package/SKILLS.md +0 -43
  23. package/STYLE_GUIDE.md +0 -610
  24. package/USER_JOURNEYS.md +0 -121
  25. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  26. package/slither-ci.config.json +0 -10
  27. package/test/721HookAttacks.t.sol +0 -408
  28. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  29. package/test/Fork.t.sol +0 -2346
  30. package/test/TestAuditGaps.sol +0 -1075
  31. package/test/TestCheckpoints.t.sol +0 -341
  32. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  33. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  34. package/test/audit/AuditRegressions.t.sol +0 -83
  35. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  36. package/test/audit/FreshAudit.t.sol +0 -197
  37. package/test/audit/FutureTierPoC.t.sol +0 -39
  38. package/test/audit/FutureTierRemoval.t.sol +0 -47
  39. package/test/audit/Pass12L18.t.sol +0 -80
  40. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  41. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  42. package/test/audit/RepoFindings.t.sol +0 -195
  43. package/test/audit/ReserveActivation.t.sol +0 -87
  44. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  45. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  46. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  47. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  48. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  49. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  50. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  51. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  52. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  53. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  54. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  55. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  56. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  57. package/test/regression/CacheTierLookup.t.sol +0 -190
  58. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  59. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  60. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  61. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  62. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  63. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  64. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  65. package/test/unit/JBBitmap.t.sol +0 -170
  66. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  67. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  68. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  69. package/test/unit/deployer_Unit.t.sol +0 -114
  70. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  71. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  72. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  73. package/test/unit/pay_Unit.t.sol +0 -1661
  74. package/test/unit/redeem_Unit.t.sol +0 -473
  75. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  76. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  77. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
@@ -1,313 +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 "./utils/UnitTestSetup.sol";
6
- import {IJB721TiersHookStore} from "../src/interfaces/IJB721TiersHookStore.sol";
7
- import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
8
-
9
- /// @title TestVotingUnitsLifecycle
10
- /// @notice Tests that voting units are correctly tracked through the full NFT lifecycle:
11
- /// mint, transfer, and burn. Verifies that the store's votingUnitsOf aggregation stays
12
- /// consistent as token ownership changes across tiers.
13
- contract TestVotingUnitsLifecycle is UnitTestSetup {
14
- using stdStorage for StdStorage;
15
-
16
- // ---------------------------------------------------------------
17
- // Test 1: Mint -> Transfer -> Burn lifecycle for voting units
18
- // ---------------------------------------------------------------
19
- /// @notice Verifies that custom voting units (useVotingUnits=true) are correctly tracked
20
- /// through the full lifecycle: mint to user A, transfer to user B, burn by user B.
21
- function test_votingUnits_mintTransferBurn_lifecycle() public {
22
- // Configure a tier with custom voting units.
23
- defaultTierConfig.flags.allowOwnerMint = true;
24
- defaultTierConfig.reserveFrequency = 0;
25
- defaultTierConfig.flags.useVotingUnits = true;
26
- defaultTierConfig.votingUnits = 100;
27
-
28
- ForTest_JB721TiersHook testHook = _initializeForTestHook(1);
29
- IJB721TiersHookStore hookStore = testHook.STORE();
30
-
31
- address userA = makeAddr("userA");
32
- address userB = makeAddr("userB");
33
-
34
- // --- Mint: NFT goes to userA ---
35
- uint16[] memory tiersToMint = new uint16[](1);
36
- tiersToMint[0] = 1;
37
- vm.prank(owner);
38
- testHook.mintFor(tiersToMint, userA);
39
-
40
- uint256 tokenId = _generateTokenId(1, 1);
41
-
42
- // Verify userA has 100 voting units.
43
- assertEq(
44
- hookStore.votingUnitsOf(address(testHook), userA), 100, "UserA should have 100 voting units after mint"
45
- );
46
- assertEq(hookStore.votingUnitsOf(address(testHook), userB), 0, "UserB should have 0 voting units after mint");
47
-
48
- // --- Transfer: NFT from userA to userB ---
49
- vm.prank(userA);
50
- IERC721(address(testHook)).transferFrom(userA, userB, tokenId);
51
-
52
- // Verify voting units moved from userA to userB.
53
- assertEq(
54
- hookStore.votingUnitsOf(address(testHook), userA), 0, "UserA should have 0 voting units after transfer"
55
- );
56
- assertEq(
57
- hookStore.votingUnitsOf(address(testHook), userB), 100, "UserB should have 100 voting units after transfer"
58
- );
59
-
60
- // --- Burn: userB burns the NFT ---
61
- uint256[] memory tokensToBurn = new uint256[](1);
62
- tokensToBurn[0] = tokenId;
63
- vm.prank(address(0)); // burn uses _burn internally via ForTest
64
- testHook.burn(tokensToBurn);
65
-
66
- // Verify both users have 0 voting units after burn.
67
- assertEq(hookStore.votingUnitsOf(address(testHook), userA), 0, "UserA should have 0 voting units after burn");
68
- assertEq(hookStore.votingUnitsOf(address(testHook), userB), 0, "UserB should have 0 voting units after burn");
69
- }
70
-
71
- // ---------------------------------------------------------------
72
- // Test 2: Multi-tier voting units aggregation
73
- // ---------------------------------------------------------------
74
- /// @notice Verifies that voting units from multiple tiers aggregate correctly
75
- /// and update properly when NFTs are transferred between users.
76
- function test_votingUnits_multiTier_aggregation() public {
77
- // Configure tiers with different custom voting units.
78
- defaultTierConfig.flags.allowOwnerMint = true;
79
- defaultTierConfig.reserveFrequency = 0;
80
- defaultTierConfig.flags.useVotingUnits = true;
81
-
82
- ForTest_JB721TiersHook testHook = _initializeForTestHook(3);
83
- IJB721TiersHookStore hookStore = testHook.STORE();
84
-
85
- // Set custom voting units for each tier.
86
- // Tier 1 = 100, Tier 2 = 200, Tier 3 = 500.
87
- testHook.test_store().ForTest_setTierVotingUnits(address(testHook), 1, 100);
88
- testHook.test_store().ForTest_setTierVotingUnits(address(testHook), 2, 200);
89
- testHook.test_store().ForTest_setTierVotingUnits(address(testHook), 3, 500);
90
-
91
- address user = makeAddr("user");
92
- address recipient = makeAddr("recipient");
93
-
94
- // --- Mint one NFT from each tier to the same user ---
95
- uint16[] memory tier1 = new uint16[](1);
96
- tier1[0] = 1;
97
- uint16[] memory tier2 = new uint16[](1);
98
- tier2[0] = 2;
99
- uint16[] memory tier3 = new uint16[](1);
100
- tier3[0] = 3;
101
-
102
- vm.startPrank(owner);
103
- testHook.mintFor(tier1, user);
104
- testHook.mintFor(tier2, user);
105
- testHook.mintFor(tier3, user);
106
- vm.stopPrank();
107
-
108
- // Verify total voting units = 100 + 200 + 500 = 800.
109
- assertEq(hookStore.votingUnitsOf(address(testHook), user), 800, "User should have 800 voting units total");
110
-
111
- // --- Transfer tier 3 NFT (500 units) to recipient ---
112
- uint256 tier3TokenId = _generateTokenId(3, 1);
113
- vm.prank(user);
114
- IERC721(address(testHook)).transferFrom(user, recipient, tier3TokenId);
115
-
116
- // Verify user now has 300, recipient has 500.
117
- assertEq(
118
- hookStore.votingUnitsOf(address(testHook), user),
119
- 300,
120
- "User should have 300 voting units after transferring tier 3"
121
- );
122
- assertEq(
123
- hookStore.votingUnitsOf(address(testHook), recipient),
124
- 500,
125
- "Recipient should have 500 voting units from tier 3"
126
- );
127
-
128
- // --- Transfer tier 1 NFT (100 units) to recipient ---
129
- uint256 tier1TokenId = _generateTokenId(1, 1);
130
- vm.prank(user);
131
- IERC721(address(testHook)).transferFrom(user, recipient, tier1TokenId);
132
-
133
- // Verify user now has 200, recipient has 600.
134
- assertEq(
135
- hookStore.votingUnitsOf(address(testHook), user),
136
- 200,
137
- "User should have 200 voting units after transferring tier 1"
138
- );
139
- assertEq(
140
- hookStore.votingUnitsOf(address(testHook), recipient),
141
- 600,
142
- "Recipient should have 600 voting units from tiers 1 and 3"
143
- );
144
- }
145
-
146
- // ---------------------------------------------------------------
147
- // Test 3: Price-based voting units (useVotingUnits=false)
148
- // ---------------------------------------------------------------
149
- /// @notice Verifies that when useVotingUnits is false, the tier price is used as voting power.
150
- function test_votingUnits_priceBasedVoting_lifecycle() public {
151
- // Configure tiers WITHOUT custom voting units (price-based voting).
152
- defaultTierConfig.flags.allowOwnerMint = true;
153
- defaultTierConfig.reserveFrequency = 0;
154
- defaultTierConfig.flags.useVotingUnits = false;
155
- defaultTierConfig.votingUnits = 0;
156
-
157
- ForTest_JB721TiersHook testHook = _initializeForTestHook(3);
158
- IJB721TiersHookStore hookStore = testHook.STORE();
159
-
160
- address user = makeAddr("user");
161
- address recipient = makeAddr("recipient");
162
-
163
- // Default tiers have prices: tier 1 = 10, tier 2 = 20, tier 3 = 30 (from _createTiers).
164
- uint16[] memory tier1 = new uint16[](1);
165
- tier1[0] = 1;
166
- uint16[] memory tier2 = new uint16[](1);
167
- tier2[0] = 2;
168
- uint16[] memory tier3 = new uint16[](1);
169
- tier3[0] = 3;
170
-
171
- vm.startPrank(owner);
172
- testHook.mintFor(tier1, user);
173
- testHook.mintFor(tier2, user);
174
- testHook.mintFor(tier3, user);
175
- vm.stopPrank();
176
-
177
- // Verify total voting units = 10 + 20 + 30 = 60 (prices).
178
- assertEq(hookStore.votingUnitsOf(address(testHook), user), 60, "User should have 60 price-based voting units");
179
-
180
- // Transfer tier 2 (price 20).
181
- uint256 tier2TokenId = _generateTokenId(2, 1);
182
- vm.prank(user);
183
- IERC721(address(testHook)).transferFrom(user, recipient, tier2TokenId);
184
-
185
- assertEq(
186
- hookStore.votingUnitsOf(address(testHook), user),
187
- 40,
188
- "User should have 40 voting units after transferring tier 2"
189
- );
190
- assertEq(
191
- hookStore.votingUnitsOf(address(testHook), recipient),
192
- 20,
193
- "Recipient should have 20 voting units from tier 2"
194
- );
195
- }
196
-
197
- // ---------------------------------------------------------------
198
- // Test 4: Multiple NFTs from the same tier
199
- // ---------------------------------------------------------------
200
- /// @notice Verifies that voting units scale correctly when a user owns multiple NFTs from one tier.
201
- function test_votingUnits_multipleMintsSameTier() public {
202
- // Configure tier with custom voting units.
203
- defaultTierConfig.flags.allowOwnerMint = true;
204
- defaultTierConfig.reserveFrequency = 0;
205
- defaultTierConfig.flags.useVotingUnits = true;
206
- defaultTierConfig.votingUnits = 50;
207
-
208
- ForTest_JB721TiersHook testHook = _initializeForTestHook(1);
209
- IJB721TiersHookStore hookStore = testHook.STORE();
210
-
211
- address user = makeAddr("user");
212
-
213
- // Mint 3 NFTs from tier 1 to the same user.
214
- uint16[] memory tiersToMint = new uint16[](3);
215
- tiersToMint[0] = 1;
216
- tiersToMint[1] = 1;
217
- tiersToMint[2] = 1;
218
-
219
- vm.prank(owner);
220
- testHook.mintFor(tiersToMint, user);
221
-
222
- // Verify total voting units = 50 * 3 = 150.
223
- assertEq(hookStore.votingUnitsOf(address(testHook), user), 150, "User should have 150 voting units (3 x 50)");
224
-
225
- // Transfer one NFT away.
226
- address recipient = makeAddr("recipient");
227
- uint256 tokenId1 = _generateTokenId(1, 1);
228
- vm.prank(user);
229
- IERC721(address(testHook)).transferFrom(user, recipient, tokenId1);
230
-
231
- // Verify voting units: user = 100, recipient = 50.
232
- assertEq(
233
- hookStore.votingUnitsOf(address(testHook), user),
234
- 100,
235
- "User should have 100 voting units after transferring 1"
236
- );
237
- assertEq(hookStore.votingUnitsOf(address(testHook), recipient), 50, "Recipient should have 50 voting units");
238
- }
239
-
240
- // ---------------------------------------------------------------
241
- // Test 5: Voting units are zero for addresses with no NFTs
242
- // ---------------------------------------------------------------
243
- /// @notice Verifies that addresses with no NFTs always return 0 voting units.
244
- function test_votingUnits_zeroForNonHolders() public {
245
- defaultTierConfig.flags.useVotingUnits = true;
246
- defaultTierConfig.votingUnits = 100;
247
-
248
- ForTest_JB721TiersHook testHook = _initializeForTestHook(5);
249
- IJB721TiersHookStore hookStore = testHook.STORE();
250
-
251
- address nonHolder = makeAddr("nonHolder");
252
-
253
- // Verify zero voting units for an address that never held any NFTs.
254
- assertEq(hookStore.votingUnitsOf(address(testHook), nonHolder), 0, "Non-holder should have 0 voting units");
255
- }
256
-
257
- // ---------------------------------------------------------------
258
- // Test 6: Voting units with mixed tier configs
259
- // ---------------------------------------------------------------
260
- /// @notice Verifies that voting units work correctly when some tiers use custom voting units
261
- /// and others use price-based voting. The tier with useVotingUnits=false should use price.
262
- function test_votingUnits_mixedTierConfigs() public {
263
- defaultTierConfig.flags.allowOwnerMint = true;
264
- defaultTierConfig.reserveFrequency = 0;
265
- defaultTierConfig.flags.useVotingUnits = true;
266
- defaultTierConfig.votingUnits = 100;
267
-
268
- ForTest_JB721TiersHook testHook = _initializeForTestHook(3);
269
- IJB721TiersHookStore hookStore = testHook.STORE();
270
-
271
- // Override tier 2 to NOT use custom voting units (price-based).
272
- // Tier 2 price from _createTiers is 20.
273
- testHook.test_store()
274
- .ForTest_setTier(
275
- address(testHook),
276
- 2,
277
- JBStored721Tier({
278
- price: uint104(20),
279
- remainingSupply: uint32(100),
280
- initialSupply: uint32(100),
281
- reserveFrequency: uint16(0),
282
- category: uint24(100),
283
- discountPercent: uint8(0),
284
- packedBools: testHook.test_store().ForTest_packBools(true, false, false, false, false, false),
285
- splitPercent: 0
286
- })
287
- );
288
- // Clear tier 2's custom voting units (so it falls back to price).
289
- testHook.test_store().ForTest_setTierVotingUnits(address(testHook), 2, 0);
290
-
291
- address user = makeAddr("user");
292
-
293
- // Mint one NFT from each tier.
294
- uint16[] memory tier1 = new uint16[](1);
295
- tier1[0] = 1;
296
- uint16[] memory tier2 = new uint16[](1);
297
- tier2[0] = 2;
298
- uint16[] memory tier3 = new uint16[](1);
299
- tier3[0] = 3;
300
-
301
- vm.startPrank(owner);
302
- testHook.mintFor(tier1, user);
303
- testHook.mintFor(tier2, user);
304
- testHook.mintFor(tier3, user);
305
- vm.stopPrank();
306
-
307
- // Tier 1: custom voting units = 100
308
- // Tier 2: price-based = 20 (useVotingUnits=false, so uses price)
309
- // Tier 3: custom voting units = 100
310
- // Total: 100 + 20 + 100 = 220
311
- assertEq(hookStore.votingUnitsOf(address(testHook), user), 220, "User should have 220 mixed voting units");
312
- }
313
- }
@@ -1,83 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // Import the shared unit test setup which deploys a hook clone with 10 tiers.
5
- // forge-lint: disable-next-line(unaliased-plain-import)
6
- import "../utils/UnitTestSetup.sol";
7
-
8
- // Import IERC2981 to compute its interface ID for the supportsInterface test.
9
- import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
10
-
11
- /// @notice Regression tests covering three audit findings for nana-721-hook-v6.
12
- contract AuditRegressions is UnitTestSetup {
13
- // -----------------------------------------------------------------------
14
- // 1. Double-initialization guard
15
- // -----------------------------------------------------------------------
16
-
17
- /// @notice Calling initialize on an already-initialized clone must revert.
18
- function test_doubleInitialization_reverts() public {
19
- // The `hook` from setUp() is already initialized via the deployer.
20
- // Expect a revert with AlreadyInitialized carrying the existing project ID.
21
- vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_AlreadyInitialized.selector, projectId));
22
-
23
- // Attempt to initialize the hook again with valid parameters — must revert.
24
- hook.initialize(
25
- projectId,
26
- "AnotherName",
27
- "AN",
28
- baseUri,
29
- IJB721TokenUriResolver(mockTokenUriResolver),
30
- contractUri,
31
- JB721InitTiersConfig({
32
- tiers: new JB721TierConfig[](0), currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
33
- }),
34
- JB721TiersHookFlags({
35
- preventOverspending: false,
36
- issueTokensForSplits: false,
37
- noNewTiersWithReserves: false,
38
- noNewTiersWithVotes: false,
39
- noNewTiersWithOwnerMinting: false
40
- })
41
- );
42
- }
43
-
44
- // -----------------------------------------------------------------------
45
- // 2. recordSetDiscountPercentOf on a removed tier must revert
46
- // -----------------------------------------------------------------------
47
-
48
- /// @notice Setting the discount percent on a tier that has been removed must revert.
49
- function test_setDiscountPercent_removedTier_reverts() public {
50
- // The hook from setUp() has 10 tiers (IDs 1-10). Remove tier 1.
51
- uint256[] memory tierIdsToRemove = new uint256[](1);
52
-
53
- // Select tier 1 to remove.
54
- tierIdsToRemove[0] = 1;
55
-
56
- // Remove tier 1 as the hook owner.
57
- vm.prank(owner);
58
- hook.adjustTiers(new JB721TierConfig[](0), tierIdsToRemove);
59
-
60
- // Expect a revert with TierRemoved when setting discount on the removed tier.
61
- vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_TierRemoved.selector, 1));
62
-
63
- // Attempt to set a discount on the removed tier — must revert.
64
- vm.prank(owner);
65
- hook.setDiscountPercentOf(1, 50);
66
- }
67
-
68
- // -----------------------------------------------------------------------
69
- // 3. ERC-2981 supportsInterface returns false (support was removed)
70
- // -----------------------------------------------------------------------
71
-
72
- /// @notice supportsInterface must return false for IERC2981 since royalty support was removed.
73
- function test_supportsInterface_erc2981_returnsFalse() public {
74
- // Compute the IERC2981 interface ID from the imported interface.
75
- bytes4 erc2981InterfaceId = type(IERC2981).interfaceId;
76
-
77
- // Query supportsInterface on the hook.
78
- bool supported = hook.supportsInterface(erc2981InterfaceId);
79
-
80
- // Assert that ERC-2981 is NOT supported.
81
- assertFalse(supported, "ERC-2981 must not be supported after royalty removal");
82
- }
83
- }
@@ -1,123 +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 "../utils/UnitTestSetup.sol";
6
- import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
7
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
8
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
9
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
10
- import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
11
- import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
12
- import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
13
-
14
- contract CrossCurrencySplitNoPrices is UnitTestSetup {
15
- function test_crossCurrencySplit_withoutPrices_locksForwardedNativeFunds() public {
16
- JB721TiersHook noPricesOrigin = new JB721TiersHook(
17
- IJBDirectory(mockJBDirectory),
18
- IJBPermissions(mockJBPermissions),
19
- IJBPrices(address(0)),
20
- IJBRulesets(mockJBRulesets),
21
- store,
22
- IJBSplits(mockJBSplits),
23
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
24
- trustedForwarder
25
- );
26
-
27
- address noPricesProxy = makeAddr("noPricesProxy");
28
- vm.etch(noPricesProxy, address(noPricesOrigin).code);
29
- JB721TiersHook crossHook = JB721TiersHook(noPricesProxy);
30
-
31
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
32
- tierConfigs[0].price = 1 ether;
33
- tierConfigs[0].splitPercent = 500_000_000; // 50%
34
-
35
- crossHook.initialize(
36
- projectId,
37
- name,
38
- symbol,
39
- baseUri,
40
- IJB721TokenUriResolver(mockTokenUriResolver),
41
- contractUri,
42
- JB721InitTiersConfig({tiers: tierConfigs, currency: uint32(USD()), decimals: 18}),
43
- JB721TiersHookFlags({
44
- preventOverspending: false,
45
- issueTokensForSplits: false,
46
- noNewTiersWithReserves: false,
47
- noNewTiersWithVotes: false,
48
- noNewTiersWithOwnerMinting: false
49
- })
50
- );
51
-
52
- uint16[] memory tierIdsToMint = new uint16[](1);
53
- tierIdsToMint[0] = 1;
54
- bytes[] memory data = new bytes[](1);
55
- data[0] = abi.encode(true, tierIdsToMint);
56
- bytes4[] memory ids = new bytes4[](1);
57
- ids[0] = metadataHelper.getId("pay", crossHook.METADATA_ID_TARGET());
58
- bytes memory payerMetadata = metadataHelper.createMetadata(ids, data);
59
-
60
- (uint256 weight, JBPayHookSpecification[] memory hookSpecifications) = crossHook.beforePayRecordedWith(
61
- JBBeforePayRecordedContext({
62
- terminal: mockTerminalAddress,
63
- payer: beneficiary,
64
- amount: JBTokenAmount({
65
- token: JBConstants.NATIVE_TOKEN,
66
- value: 1 ether,
67
- decimals: 18,
68
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
69
- }),
70
- projectId: projectId,
71
- rulesetId: 0,
72
- beneficiary: beneficiary,
73
- weight: 10e18,
74
- reservedPercent: 0,
75
- metadata: payerMetadata
76
- })
77
- );
78
-
79
- // When PRICES is address(0) and currencies differ, convertAndCapSplitAmounts returns 0
80
- // to avoid forwarding an unconverted amount in the wrong currency denomination.
81
- // This means weight is NOT reduced (full weight) and no funds are forwarded.
82
- assertEq(weight, 10e18, "weight unchanged when split conversion fails due to missing prices");
83
- assertEq(hookSpecifications.length, 1, "one pay hook spec");
84
- assertEq(hookSpecifications[0].amount, 0, "split amount is zero when prices unavailable for conversion");
85
-
86
- mockAndExpect(
87
- address(mockJBDirectory),
88
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
89
- abi.encode(true)
90
- );
91
-
92
- JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
93
- payer: beneficiary,
94
- projectId: projectId,
95
- rulesetId: 0,
96
- amount: JBTokenAmount({
97
- token: JBConstants.NATIVE_TOKEN,
98
- value: 1 ether,
99
- decimals: 18,
100
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
101
- }),
102
- forwardedAmount: JBTokenAmount({
103
- token: JBConstants.NATIVE_TOKEN,
104
- value: hookSpecifications[0].amount,
105
- decimals: 18,
106
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
107
- }),
108
- weight: weight,
109
- newlyIssuedTokenCount: 0,
110
- beneficiary: beneficiary,
111
- hookMetadata: hookSpecifications[0].metadata,
112
- payerMetadata: payerMetadata
113
- });
114
-
115
- vm.deal(mockTerminalAddress, 1 ether);
116
- vm.prank(mockTerminalAddress);
117
- crossHook.afterPayRecordedWith{value: hookSpecifications[0].amount}(payContext);
118
-
119
- assertEq(crossHook.balanceOf(beneficiary), 0, "no NFTs minted (currency mismatch, no prices)");
120
- assertEq(crossHook.payCreditsOf(beneficiary), 0, "no credits accrued (currency mismatch, no prices)");
121
- assertEq(address(crossHook).balance, 0, "no funds forwarded to hook when split conversion returns zero");
122
- }
123
- }