@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,273 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {Test} from "forge-std/Test.sol";
5
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
6
-
7
- import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
- import {JB721Tier} from "../../src/structs/JB721Tier.sol";
9
- import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
10
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
11
-
12
- /// @notice Tests that paid mints cannot consume slots reserved for the reserve beneficiary.
13
- /// @dev Regression test for the vulnerability where `_numberOfPendingReservesFor` returns 0
14
- /// when `remainingSupply == 0` (sold-out early-return), allowing the post-decrement guard
15
- /// `0 < 0` to pass and permanently stealing reserved slots from the beneficiary.
16
- /// The fix uses `_numberOfPendingReservesForMintGuard` which omits the sold-out early-return.
17
- contract ReserveSlotProtection is Test {
18
- JB721TiersHookStore internal store;
19
-
20
- function setUp() public {
21
- store = new JB721TiersHookStore();
22
- }
23
-
24
- /// @notice Proves the fix: a paid mint that would consume the last reserved slot reverts.
25
- /// Scenario: tier with initialSupply=2, reserveFrequency=1 (1 reserve per paid mint).
26
- /// After 1 paid mint, 1 slot remains but it's reserved. Second paid mint must revert.
27
- /// Post-decrement trace: remaining goes 2->1, pending=ceil(1/1)=1, check 1<1 = false -> ok.
28
- /// Second mint: remaining goes 1->0, _numberOfPendingReservesForMintGuard returns 2
29
- /// (ceil(2/1)-0=2), check 0<2 -> true -> reverts.
30
- function test_paidMintRevertsWhenOnlyReservedSlotsRemain() public {
31
- JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
32
- tiers[0] = JB721TierConfig({
33
- price: 1 ether,
34
- initialSupply: 2,
35
- votingUnits: 0,
36
- reserveFrequency: 1,
37
- reserveBeneficiary: address(0xBEEF),
38
- encodedIPFSUri: bytes32(0),
39
- category: 0,
40
- discountPercent: 0,
41
- flags: JB721TierConfigFlags({
42
- allowOwnerMint: false,
43
- useReserveBeneficiaryAsDefault: false,
44
- transfersPausable: false,
45
- useVotingUnits: false,
46
- cantBeRemoved: false,
47
- cantIncreaseDiscountPercent: false,
48
- cantBuyWithCredits: false
49
- }),
50
- splitPercent: 0,
51
- splits: new JBSplit[](0)
52
- });
53
-
54
- store.recordAddTiers(tiers);
55
-
56
- uint16[] memory tierIds = new uint16[](1);
57
- tierIds[0] = 1;
58
-
59
- // First paid mint succeeds — uses 1 of 2 slots, leaving 1 for the pending reserve.
60
- store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
61
-
62
- // Verify state: 1 remaining supply, 1 pending reserve.
63
- JB721Tier memory tier = store.tierOf(address(this), 1, false);
64
- assertEq(tier.remainingSupply, 1, "should have 1 slot remaining after first mint");
65
- assertEq(store.numberOfPendingReservesFor(address(this), 1), 1, "should have 1 pending reserve");
66
-
67
- // Second paid mint MUST revert — the only remaining slot belongs to the reserve beneficiary.
68
- vm.expectRevert(
69
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
70
- );
71
- store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
72
- }
73
-
74
- /// @notice Proves that the reserve beneficiary can still mint their reserved slot after fix.
75
- function test_reserveBeneficiaryCanMintAfterPaidSlotsExhausted() public {
76
- JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
77
- tiers[0] = JB721TierConfig({
78
- price: 1 ether,
79
- initialSupply: 2,
80
- votingUnits: 0,
81
- reserveFrequency: 1,
82
- reserveBeneficiary: address(0xBEEF),
83
- encodedIPFSUri: bytes32(0),
84
- category: 0,
85
- discountPercent: 0,
86
- flags: JB721TierConfigFlags({
87
- allowOwnerMint: false,
88
- useReserveBeneficiaryAsDefault: false,
89
- transfersPausable: false,
90
- useVotingUnits: false,
91
- cantBeRemoved: false,
92
- cantIncreaseDiscountPercent: false,
93
- cantBuyWithCredits: false
94
- }),
95
- splitPercent: 0,
96
- splits: new JBSplit[](0)
97
- });
98
-
99
- store.recordAddTiers(tiers);
100
-
101
- uint16[] memory tierIds = new uint16[](1);
102
- tierIds[0] = 1;
103
-
104
- // First paid mint.
105
- store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
106
-
107
- // Reserve beneficiary mints their entitled reserve.
108
- uint256[] memory reserveTokenIds = store.recordMintReservesFor({tierId: 1, count: 1});
109
- assertEq(reserveTokenIds.length, 1, "reserve beneficiary should get 1 token");
110
-
111
- // After reserve mint, remaining supply is 0 and reserves are fulfilled.
112
- JB721Tier memory tier = store.tierOf(address(this), 1, false);
113
- assertEq(tier.remainingSupply, 0, "tier should be fully minted");
114
- assertEq(store.numberOfReservesMintedFor(address(this), 1), 1, "1 reserve should be minted");
115
- }
116
-
117
- /// @notice Tests a larger tier: initialSupply=10, reserveFrequency=2.
118
- /// With frequency=2, every 2 paid mints earn 1 reserve (rounded up).
119
- /// Post-decrement analysis for the 7th mint:
120
- /// remaining goes 4->3, nonReserveMints=7, pending=ceil(7/2)-0=4.
121
- /// Check: 3 < 4 -> true -> reverts. Correct!
122
- /// So mints 1-6 succeed, mint 7 reverts.
123
- function test_largerTierReserveProtection() public {
124
- JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
125
- tiers[0] = JB721TierConfig({
126
- price: 0.1 ether,
127
- initialSupply: 10,
128
- votingUnits: 0,
129
- reserveFrequency: 2,
130
- reserveBeneficiary: address(0xCAFE),
131
- encodedIPFSUri: bytes32(0),
132
- category: 0,
133
- discountPercent: 0,
134
- flags: JB721TierConfigFlags({
135
- allowOwnerMint: false,
136
- useReserveBeneficiaryAsDefault: false,
137
- transfersPausable: false,
138
- useVotingUnits: false,
139
- cantBeRemoved: false,
140
- cantIncreaseDiscountPercent: false,
141
- cantBuyWithCredits: false
142
- }),
143
- splitPercent: 0,
144
- splits: new JBSplit[](0)
145
- });
146
-
147
- store.recordAddTiers(tiers);
148
-
149
- uint16[] memory tierIds = new uint16[](1);
150
- tierIds[0] = 1;
151
-
152
- // Mint 6 paid NFTs. After 6 mints: remaining=4, pending=ceil(6/2)=3.
153
- for (uint256 i; i < 6; i++) {
154
- store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
155
- }
156
-
157
- JB721Tier memory tier = store.tierOf(address(this), 1, false);
158
- assertEq(tier.remainingSupply, 4, "should have 4 remaining after 6 mints");
159
- assertEq(store.numberOfPendingReservesFor(address(this), 1), 3, "should have 3 pending reserves");
160
-
161
- // 7th paid mint should revert: after decrement, remaining=3, pending=ceil(7/2)=4. 3<4 -> reverts.
162
- vm.expectRevert(
163
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
164
- );
165
- store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
166
- }
167
-
168
- /// @notice Verifies that when reserves are minted between paid mints, paid mints can continue.
169
- /// initialSupply=4, reserveFrequency=1.
170
- function test_paidMintsResumeAfterReservesMinted() public {
171
- JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
172
- tiers[0] = JB721TierConfig({
173
- price: 1 ether,
174
- initialSupply: 4,
175
- votingUnits: 0,
176
- reserveFrequency: 1,
177
- reserveBeneficiary: address(0xBEEF),
178
- encodedIPFSUri: bytes32(0),
179
- category: 0,
180
- discountPercent: 0,
181
- flags: JB721TierConfigFlags({
182
- allowOwnerMint: false,
183
- useReserveBeneficiaryAsDefault: false,
184
- transfersPausable: false,
185
- useVotingUnits: false,
186
- cantBeRemoved: false,
187
- cantIncreaseDiscountPercent: false,
188
- cantBuyWithCredits: false
189
- }),
190
- splitPercent: 0,
191
- splits: new JBSplit[](0)
192
- });
193
-
194
- store.recordAddTiers(tiers);
195
-
196
- uint16[] memory tierIds = new uint16[](1);
197
- tierIds[0] = 1;
198
-
199
- // Paid mint 1: remaining 4->3, nonReserveMints=1, pending=ceil(1/1)=1. Check: 3<1? No. OK.
200
- store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
201
-
202
- // Paid mint 2: remaining 3->2, nonReserveMints=2, pending=ceil(2/1)=2. Check: 2<2? No. OK.
203
- store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
204
-
205
- // Paid mint 3: remaining 2->1, nonReserveMints=3, pending=ceil(3/1)=3. Check: 1<3? Yes! Revert.
206
- vm.expectRevert(
207
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
208
- );
209
- store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
210
-
211
- // Mint 1 reserve — frees up a slot. Now reservesMinted=1.
212
- store.recordMintReservesFor({tierId: 1, count: 1});
213
-
214
- // State: remaining=1, reservesMinted=1, nonReserveMints=2.
215
- // Paid mint attempt: remaining 1->0, nonReserveMints=3, pending=ceil(3/1)-1=2. Check: 0<2? Yes! Revert.
216
- vm.expectRevert(
217
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
218
- );
219
- store.recordMint({amount: 1 ether, tierIds: tierIds, isOwnerMint: false});
220
-
221
- // Mint remaining reserve.
222
- store.recordMintReservesFor({tierId: 1, count: 1});
223
-
224
- // Tier fully minted — 0 remaining.
225
- JB721Tier memory tier = store.tierOf(address(this), 1, false);
226
- assertEq(tier.remainingSupply, 0, "tier should be fully minted");
227
- }
228
-
229
- /// @notice Without reserves, all NFTs in a tier should be mintable (no off-by-one).
230
- function test_noReservesFullSupplyMintable() public {
231
- JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
232
- tiers[0] = JB721TierConfig({
233
- price: 0.1 ether,
234
- initialSupply: 5,
235
- votingUnits: 0,
236
- reserveFrequency: 0,
237
- reserveBeneficiary: address(0),
238
- encodedIPFSUri: bytes32(0),
239
- category: 0,
240
- discountPercent: 0,
241
- flags: JB721TierConfigFlags({
242
- allowOwnerMint: false,
243
- useReserveBeneficiaryAsDefault: false,
244
- transfersPausable: false,
245
- useVotingUnits: false,
246
- cantBeRemoved: false,
247
- cantIncreaseDiscountPercent: false,
248
- cantBuyWithCredits: false
249
- }),
250
- splitPercent: 0,
251
- splits: new JBSplit[](0)
252
- });
253
-
254
- store.recordAddTiers(tiers);
255
-
256
- uint16[] memory tierIds = new uint16[](1);
257
- tierIds[0] = 1;
258
-
259
- // Mint all 5 — should succeed since no reserves to protect.
260
- for (uint256 i; i < 5; i++) {
261
- store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
262
- }
263
-
264
- JB721Tier memory tier = store.tierOf(address(this), 1, false);
265
- assertEq(tier.remainingSupply, 0, "fully minted");
266
-
267
- // 6th mint should revert (supply exhausted).
268
- vm.expectRevert(
269
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1)
270
- );
271
- store.recordMint({amount: 0.1 ether, tierIds: tierIds, isOwnerMint: false});
272
- }
273
- }
@@ -1,149 +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 {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
- import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
8
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
9
- import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
10
-
11
- contract RetroactiveReserveBeneficiaryDilution is UnitTestSetup {
12
- /// @notice Creating a tier with reserveFrequency > 0 but no beneficiary (tier-specific or default) now reverts,
13
- /// preventing the phantom reserve scenario where a later default beneficiary retroactively inflates
14
- /// totalCashOutWeight and dilutes existing holders.
15
- function test_adjustTier_reverts_when_reserve_has_no_beneficiary() public {
16
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
17
-
18
- JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
19
- tier1[0] = JB721TierConfig({
20
- price: 1 ether,
21
- initialSupply: 100,
22
- votingUnits: 0,
23
- reserveFrequency: 2,
24
- reserveBeneficiary: address(0),
25
- encodedIPFSUri: bytes32(uint256(0x1234)),
26
- category: 1,
27
- discountPercent: 0,
28
- flags: JB721TierConfigFlags({
29
- allowOwnerMint: false,
30
- useReserveBeneficiaryAsDefault: false,
31
- transfersPausable: false,
32
- useVotingUnits: false,
33
- cantBeRemoved: false,
34
- cantIncreaseDiscountPercent: false,
35
- cantBuyWithCredits: false
36
- }),
37
- splitPercent: 0,
38
- splits: new JBSplit[](0)
39
- });
40
-
41
- vm.prank(owner);
42
- vm.expectRevert(
43
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
44
- );
45
- testHook.adjustTiers(tier1, new uint256[](0));
46
- }
47
-
48
- /// @notice A tier with reserveFrequency > 0 succeeds when an explicit beneficiary is provided.
49
- function test_adjustTier_succeeds_with_explicit_reserve_beneficiary() public {
50
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
51
-
52
- JB721TierConfig[] memory tier1 = new JB721TierConfig[](1);
53
- tier1[0] = JB721TierConfig({
54
- price: 1 ether,
55
- initialSupply: 100,
56
- votingUnits: 0,
57
- reserveFrequency: 2,
58
- reserveBeneficiary: owner,
59
- encodedIPFSUri: bytes32(uint256(0x1234)),
60
- category: 1,
61
- discountPercent: 0,
62
- flags: JB721TierConfigFlags({
63
- allowOwnerMint: false,
64
- useReserveBeneficiaryAsDefault: false,
65
- transfersPausable: false,
66
- useVotingUnits: false,
67
- cantBeRemoved: false,
68
- cantIncreaseDiscountPercent: false,
69
- cantBuyWithCredits: false
70
- }),
71
- splitPercent: 0,
72
- splits: new JBSplit[](0)
73
- });
74
-
75
- vm.prank(owner);
76
- testHook.adjustTiers(tier1, new uint256[](0));
77
-
78
- assertEq(
79
- testHook.STORE().reserveBeneficiaryOf(address(testHook), 1),
80
- owner,
81
- "tier-specific beneficiary should be set"
82
- );
83
- }
84
-
85
- /// @notice A tier with reserveFrequency > 0 and no explicit beneficiary succeeds when a default beneficiary
86
- /// was previously set by an earlier tier.
87
- function test_adjustTier_succeeds_with_default_reserve_beneficiary() public {
88
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
89
-
90
- // Add two tiers: first sets the default beneficiary, second relies on it.
91
- JB721TierConfig[] memory tiers = new JB721TierConfig[](2);
92
-
93
- // Tier 1: sets default beneficiary via useReserveBeneficiaryAsDefault.
94
- tiers[0] = JB721TierConfig({
95
- price: 1 ether,
96
- initialSupply: 100,
97
- votingUnits: 0,
98
- reserveFrequency: 2,
99
- reserveBeneficiary: owner,
100
- encodedIPFSUri: bytes32(uint256(0x1234)),
101
- category: 1,
102
- discountPercent: 0,
103
- flags: JB721TierConfigFlags({
104
- allowOwnerMint: false,
105
- useReserveBeneficiaryAsDefault: true,
106
- transfersPausable: false,
107
- useVotingUnits: false,
108
- cantBeRemoved: false,
109
- cantIncreaseDiscountPercent: false,
110
- cantBuyWithCredits: false
111
- }),
112
- splitPercent: 0,
113
- splits: new JBSplit[](0)
114
- });
115
-
116
- // Tier 2: relies on the default beneficiary (no explicit one).
117
- tiers[1] = JB721TierConfig({
118
- price: 2 ether,
119
- initialSupply: 100,
120
- votingUnits: 0,
121
- reserveFrequency: 3,
122
- reserveBeneficiary: address(0),
123
- encodedIPFSUri: bytes32(uint256(0x5678)),
124
- category: 2,
125
- discountPercent: 0,
126
- flags: JB721TierConfigFlags({
127
- allowOwnerMint: false,
128
- useReserveBeneficiaryAsDefault: false,
129
- transfersPausable: false,
130
- useVotingUnits: false,
131
- cantBeRemoved: false,
132
- cantIncreaseDiscountPercent: false,
133
- cantBuyWithCredits: false
134
- }),
135
- splitPercent: 0,
136
- splits: new JBSplit[](0)
137
- });
138
-
139
- vm.prank(owner);
140
- testHook.adjustTiers(tiers, new uint256[](0));
141
-
142
- // Tier 2 should inherit the default beneficiary set by tier 1.
143
- assertEq(
144
- testHook.STORE().reserveBeneficiaryOf(address(testHook), 2),
145
- owner,
146
- "tier 2 should inherit default beneficiary"
147
- );
148
- }
149
- }
@@ -1,249 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
5
- import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
6
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
7
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
- import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
9
- import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
10
- import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
11
- import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
12
- import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
13
- import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
14
- import {JB721TiersHook} from "../../src/JB721TiersHook.sol";
15
- import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
16
- import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
17
- import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
18
- import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
19
- import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
20
-
21
- /// @notice Regression test for: same-currency decimal mismatch in split forwarding.
22
- /// @dev When pricing decimals differ from payment decimals but the currency is the same,
23
- /// `convertAndCapSplitAmounts` must rescale split amounts before comparing to `amountValue`.
24
- /// Without the fix, split amounts stay in pricing decimals (e.g. 18), the cap comparison uses
25
- /// payment decimals (e.g. 6), and the cap clips the split to 100% of the payment.
26
- contract SameCurrencyDecimalMismatch is UnitTestSetup {
27
- // Shared constants.
28
- address constant MOCK_TOKEN = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
29
- // forge-lint: disable-next-line(unsafe-typecast)
30
- uint32 constant CURRENCY = uint32(uint160(MOCK_TOKEN));
31
-
32
- /// @notice Prove that a 50% split with same currency but different decimals (pricing=18, payment=6)
33
- /// correctly forwards ~50% of the payment, not 100%.
34
- function test_sameCurrency_differentDecimals_splitAmountScaledCorrectly() public {
35
- // Deploy hook with PRICES=address(0), tier priced at 1e18 (18-decimal), 50% split.
36
- JB721TiersHook testHook;
37
- {
38
- JB721TiersHook origin = new JB721TiersHook(
39
- IJBDirectory(mockJBDirectory),
40
- IJBPermissions(mockJBPermissions),
41
- IJBPrices(address(0)),
42
- IJBRulesets(mockJBRulesets),
43
- store,
44
- IJBSplits(mockJBSplits),
45
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
46
- trustedForwarder
47
- );
48
- address hookAddr = makeAddr("hook18to6");
49
- vm.etch(hookAddr, address(origin).code);
50
- testHook = JB721TiersHook(hookAddr);
51
- }
52
-
53
- {
54
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
55
- tierConfigs[0].price = 1e18;
56
- tierConfigs[0].splitPercent = 500_000_000; // 50%.
57
- testHook.initialize(
58
- projectId,
59
- name,
60
- symbol,
61
- baseUri,
62
- IJB721TokenUriResolver(mockTokenUriResolver),
63
- contractUri,
64
- JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
65
- JB721TiersHookFlags({
66
- preventOverspending: false,
67
- issueTokensForSplits: false,
68
- noNewTiersWithReserves: false,
69
- noNewTiersWithVotes: false,
70
- noNewTiersWithOwnerMinting: false
71
- })
72
- );
73
- }
74
-
75
- // Build payer metadata requesting tier 1.
76
- bytes memory payerMetadata;
77
- {
78
- uint16[] memory tierIdsToMint = new uint16[](1);
79
- tierIdsToMint[0] = 1;
80
- bytes[] memory data = new bytes[](1);
81
- data[0] = abi.encode(true, tierIdsToMint);
82
- bytes4[] memory ids = new bytes4[](1);
83
- ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
84
- payerMetadata = metadataHelper.createMetadata(ids, data);
85
- }
86
-
87
- // Pay 1.0 token reported as 6 decimals (value = 1e6). Same currency, different decimals.
88
- (uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
89
- JBBeforePayRecordedContext({
90
- terminal: mockTerminalAddress,
91
- payer: beneficiary,
92
- amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e6, decimals: 6, currency: CURRENCY}),
93
- projectId: projectId,
94
- rulesetId: 0,
95
- beneficiary: beneficiary,
96
- weight: 10e18,
97
- reservedPercent: 0,
98
- metadata: payerMetadata
99
- })
100
- );
101
-
102
- // Without the fix: split amount (5e17 in 18-decimal pricing) is compared to amountValue (1e6),
103
- // causing the cap to clip it to 1e6 (100% of payment) and weight becomes 0.
104
- // With the fix: split is rescaled to 5e5 (50% of 1e6) and weight is 5e18.
105
- assertEq(hookSpecs[0].amount, 5e5, "split should be 50% of payment (5e5), not capped to 100%");
106
- assertEq(weight, 5e18, "weight should be 50% (half goes to splits)");
107
- }
108
-
109
- /// @notice Sanity check: same currency AND same decimals — no rescaling needed.
110
- function test_sameCurrency_sameDecimals_splitAmountUnchanged() public {
111
- JB721TiersHook testHook;
112
- {
113
- JB721TiersHook origin = new JB721TiersHook(
114
- IJBDirectory(mockJBDirectory),
115
- IJBPermissions(mockJBPermissions),
116
- IJBPrices(address(0)),
117
- IJBRulesets(mockJBRulesets),
118
- store,
119
- IJBSplits(mockJBSplits),
120
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
121
- trustedForwarder
122
- );
123
- address hookAddr = makeAddr("hook18to18");
124
- vm.etch(hookAddr, address(origin).code);
125
- testHook = JB721TiersHook(hookAddr);
126
- }
127
-
128
- {
129
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
130
- tierConfigs[0].price = 1e18;
131
- tierConfigs[0].splitPercent = 500_000_000;
132
- testHook.initialize(
133
- projectId,
134
- name,
135
- symbol,
136
- baseUri,
137
- IJB721TokenUriResolver(mockTokenUriResolver),
138
- contractUri,
139
- JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
140
- JB721TiersHookFlags({
141
- preventOverspending: false,
142
- issueTokensForSplits: false,
143
- noNewTiersWithReserves: false,
144
- noNewTiersWithVotes: false,
145
- noNewTiersWithOwnerMinting: false
146
- })
147
- );
148
- }
149
-
150
- bytes memory payerMetadata;
151
- {
152
- uint16[] memory tierIdsToMint = new uint16[](1);
153
- tierIdsToMint[0] = 1;
154
- bytes[] memory data = new bytes[](1);
155
- data[0] = abi.encode(true, tierIdsToMint);
156
- bytes4[] memory ids = new bytes4[](1);
157
- ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
158
- payerMetadata = metadataHelper.createMetadata(ids, data);
159
- }
160
-
161
- (uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
162
- JBBeforePayRecordedContext({
163
- terminal: mockTerminalAddress,
164
- payer: beneficiary,
165
- amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
166
- projectId: projectId,
167
- rulesetId: 0,
168
- beneficiary: beneficiary,
169
- weight: 10e18,
170
- reservedPercent: 0,
171
- metadata: payerMetadata
172
- })
173
- );
174
-
175
- assertEq(hookSpecs[0].amount, 5e17, "split should be 50% of payment");
176
- assertEq(weight, 5e18, "weight should be 50%");
177
- }
178
-
179
- /// @notice Same currency, payment has MORE decimals than pricing (pricing=6, payment=18).
180
- function test_sameCurrency_paymentMoreDecimals_splitScaledUp() public {
181
- JB721TiersHook testHook;
182
- {
183
- JB721TiersHook origin = new JB721TiersHook(
184
- IJBDirectory(mockJBDirectory),
185
- IJBPermissions(mockJBPermissions),
186
- IJBPrices(address(0)),
187
- IJBRulesets(mockJBRulesets),
188
- store,
189
- IJBSplits(mockJBSplits),
190
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
191
- trustedForwarder
192
- );
193
- address hookAddr = makeAddr("hook6to18");
194
- vm.etch(hookAddr, address(origin).code);
195
- testHook = JB721TiersHook(hookAddr);
196
- }
197
-
198
- {
199
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
200
- tierConfigs[0].price = 1e6; // 1.0 token in 6-decimal pricing.
201
- tierConfigs[0].splitPercent = 500_000_000;
202
- testHook.initialize(
203
- projectId,
204
- name,
205
- symbol,
206
- baseUri,
207
- IJB721TokenUriResolver(mockTokenUriResolver),
208
- contractUri,
209
- JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 6}),
210
- JB721TiersHookFlags({
211
- preventOverspending: false,
212
- issueTokensForSplits: false,
213
- noNewTiersWithReserves: false,
214
- noNewTiersWithVotes: false,
215
- noNewTiersWithOwnerMinting: false
216
- })
217
- );
218
- }
219
-
220
- bytes memory payerMetadata;
221
- {
222
- uint16[] memory tierIdsToMint = new uint16[](1);
223
- tierIdsToMint[0] = 1;
224
- bytes[] memory data = new bytes[](1);
225
- data[0] = abi.encode(true, tierIdsToMint);
226
- bytes4[] memory ids = new bytes4[](1);
227
- ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
228
- payerMetadata = metadataHelper.createMetadata(ids, data);
229
- }
230
-
231
- (uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
232
- JBBeforePayRecordedContext({
233
- terminal: mockTerminalAddress,
234
- payer: beneficiary,
235
- amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
236
- projectId: projectId,
237
- rulesetId: 0,
238
- beneficiary: beneficiary,
239
- weight: 10e18,
240
- reservedPercent: 0,
241
- metadata: payerMetadata
242
- })
243
- );
244
-
245
- // 50% of 1.0 token in 18-decimal payment = 5e17.
246
- assertEq(hookSpecs[0].amount, 5e17, "split scaled up to 18-decimal payment");
247
- assertEq(weight, 5e18, "weight should be 50%");
248
- }
249
- }