@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,266 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import "../utils/UnitTestSetup.sol";
5
-
6
- import {JB721TiersHookProjectDeployer} from "../../src/JB721TiersHookProjectDeployer.sol";
7
- import {JBDeploy721TiersHookConfig} from "../../src/structs/JBDeploy721TiersHookConfig.sol";
8
- import {JBLaunchRulesetsConfig} from "../../src/structs/JBLaunchRulesetsConfig.sol";
9
- import {JBQueueRulesetsConfig} from "../../src/structs/JBQueueRulesetsConfig.sol";
10
- import {JBPayDataHookRulesetConfig} from "../../src/structs/JBPayDataHookRulesetConfig.sol";
11
- import {JBPayDataHookRulesetMetadata} from "../../src/structs/JBPayDataHookRulesetMetadata.sol";
12
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
13
- import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
14
- import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
15
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
16
- import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
17
- import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
18
- import {IJBRulesetApprovalHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetApprovalHook.sol";
19
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
20
- import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
21
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
22
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
23
- import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
24
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
25
-
26
- contract MockProjects {
27
- uint256 internal _count;
28
- address internal _owner;
29
-
30
- function setup(uint256 count_, address owner_) external {
31
- _count = count_;
32
- _owner = owner_;
33
- }
34
-
35
- function count() external view returns (uint256) {
36
- return _count;
37
- }
38
-
39
- function ownerOf(uint256) external view returns (address) {
40
- return _owner;
41
- }
42
-
43
- fallback() external {}
44
- }
45
-
46
- contract StrictController {
47
- address internal immutable EXPECTED_CALLER;
48
-
49
- error UnexpectedCaller(address caller);
50
-
51
- constructor(address expectedCaller) {
52
- EXPECTED_CALLER = expectedCaller;
53
- }
54
-
55
- fallback() external payable {
56
- if (msg.sender != EXPECTED_CALLER) revert UnexpectedCaller(msg.sender);
57
- bytes memory result = abi.encode(uint256(42));
58
- assembly {
59
- return(add(result, 32), mload(result))
60
- }
61
- }
62
- }
63
-
64
- contract Test_ProjectDeployerAuth is UnitTestSetup {
65
- JB721TiersHookProjectDeployer internal projectDeployer;
66
- address internal operator = address(0xBEEF);
67
- uint256 internal testProjectId = 5;
68
-
69
- function setUp() public override {
70
- super.setUp();
71
-
72
- MockProjects projects = new MockProjects();
73
- vm.etch(mockJBProjects, address(projects).code);
74
- MockProjects(mockJBProjects).setup(testProjectId, owner);
75
-
76
- vm.mockCall(mockJBDirectory, abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(mockJBProjects));
77
-
78
- projectDeployer = new JB721TiersHookProjectDeployer(
79
- IJBDirectory(mockJBDirectory), IJBPermissions(mockJBPermissions), jbHookDeployer, address(0)
80
- );
81
- }
82
-
83
- function test_launchRulesetsFor_revertsBecauseControllerSeesProjectDeployerAsCaller() external {
84
- (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
85
- _launchConfig(testProjectId);
86
-
87
- StrictController controller = new StrictController(owner);
88
-
89
- vm.prank(owner);
90
- vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
91
- projectDeployer.launchRulesetsFor(
92
- testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
93
- );
94
- }
95
-
96
- function test_queueRulesetsOf_revertsBecauseControllerSeesProjectDeployerAsCaller() external {
97
- (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
98
- _queueConfig(testProjectId);
99
-
100
- StrictController controller = new StrictController(owner);
101
-
102
- vm.prank(owner);
103
- vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
104
- projectDeployer.queueRulesetsOf(
105
- testProjectId, hookConfig, queueConfig, IJBController(address(controller)), bytes32(0)
106
- );
107
- }
108
-
109
- function test_launchRulesetsFor_checksLaunchPermission() external {
110
- (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
111
- _launchConfig(testProjectId);
112
-
113
- address account = owner;
114
- StrictController controller = new StrictController(owner);
115
-
116
- // Grant LAUNCH_RULESETS and SET_TERMINALS, deny QUEUE_RULESETS.
117
- vm.mockCall(
118
- mockJBPermissions,
119
- abi.encodeWithSelector(
120
- IJBPermissions.hasPermission.selector,
121
- operator,
122
- account,
123
- testProjectId,
124
- JBPermissionIds.QUEUE_RULESETS,
125
- true,
126
- true
127
- ),
128
- abi.encode(false)
129
- );
130
- vm.mockCall(
131
- mockJBPermissions,
132
- abi.encodeWithSelector(
133
- IJBPermissions.hasPermission.selector,
134
- operator,
135
- account,
136
- testProjectId,
137
- JBPermissionIds.LAUNCH_RULESETS,
138
- true,
139
- true
140
- ),
141
- abi.encode(true)
142
- );
143
- vm.mockCall(
144
- mockJBPermissions,
145
- abi.encodeWithSelector(
146
- IJBPermissions.hasPermission.selector,
147
- operator,
148
- account,
149
- testProjectId,
150
- JBPermissionIds.SET_TERMINALS,
151
- true,
152
- true
153
- ),
154
- abi.encode(true)
155
- );
156
-
157
- // The permission check passes with LAUNCH_RULESETS. The call proceeds to the controller, which reverts
158
- // because it sees the deployer contract as the caller rather than the original operator.
159
- vm.prank(operator);
160
- vm.expectRevert(abi.encodeWithSelector(StrictController.UnexpectedCaller.selector, address(projectDeployer)));
161
- projectDeployer.launchRulesetsFor(
162
- testProjectId, hookConfig, launchConfig, IJBController(address(controller)), bytes32(0)
163
- );
164
- }
165
-
166
- function _launchConfig(uint256 projectId_)
167
- internal
168
- view
169
- returns (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig)
170
- {
171
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
172
- tierConfigs[0] = JB721TierConfig({
173
- price: uint104(10),
174
- initialSupply: uint32(100),
175
- votingUnits: uint16(0),
176
- reserveFrequency: uint16(0),
177
- reserveBeneficiary: reserveBeneficiary,
178
- encodedIPFSUri: tokenUris[0],
179
- category: uint24(1),
180
- discountPercent: uint8(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
- hookConfig = JBDeploy721TiersHookConfig({
195
- name: name,
196
- symbol: symbol,
197
- baseUri: baseUri,
198
- tokenUriResolver: IJB721TokenUriResolver(address(0)),
199
- contractUri: contractUri,
200
- tiersConfig: JB721InitTiersConfig({
201
- tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
202
- }),
203
- flags: JB721TiersHookFlags({
204
- preventOverspending: false,
205
- issueTokensForSplits: false,
206
- noNewTiersWithReserves: true,
207
- noNewTiersWithVotes: false,
208
- noNewTiersWithOwnerMinting: true
209
- })
210
- });
211
-
212
- JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
213
- rulesetConfigs[0].mustStartAtOrAfter = 0;
214
- rulesetConfigs[0].duration = 14;
215
- rulesetConfigs[0].weight = 1e18;
216
- rulesetConfigs[0].weightCutPercent = 0;
217
- rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
218
- rulesetConfigs[0].metadata = JBPayDataHookRulesetMetadata({
219
- reservedPercent: 5000,
220
- cashOutTaxRate: 5000,
221
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
222
- pausePay: false,
223
- pauseCreditTransfers: false,
224
- allowOwnerMinting: false,
225
- allowSetCustomToken: false,
226
- allowTerminalMigration: false,
227
- allowSetTerminals: false,
228
- allowSetController: false,
229
- ownerMustSendPayouts: false,
230
- allowAddAccountingContext: false,
231
- allowAddPriceFeed: false,
232
- holdFees: false,
233
- useTotalSurplusForCashOuts: false,
234
- useDataHookForCashOut: false,
235
- metadata: 0x00
236
- });
237
-
238
- JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
239
- accountingContexts[0] = JBAccountingContext({
240
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18, token: JBConstants.NATIVE_TOKEN
241
- });
242
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
243
- terminalConfigs[0] = JBTerminalConfig({
244
- terminal: IJBTerminal(mockTerminalAddress), accountingContextsToAccept: accountingContexts
245
- });
246
-
247
- launchConfig = JBLaunchRulesetsConfig({
248
- projectId: uint56(projectId_),
249
- rulesetConfigurations: rulesetConfigs,
250
- terminalConfigurations: terminalConfigs,
251
- memo: "launch"
252
- });
253
- }
254
-
255
- function _queueConfig(uint256 projectId_)
256
- internal
257
- view
258
- returns (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig)
259
- {
260
- JBLaunchRulesetsConfig memory launchConfig;
261
- (hookConfig, launchConfig) = _launchConfig(projectId_);
262
- queueConfig = JBQueueRulesetsConfig({
263
- projectId: uint56(projectId_), rulesetConfigurations: launchConfig.rulesetConfigurations, memo: "queue"
264
- });
265
- }
266
- }
@@ -1,195 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import "../utils/UnitTestSetup.sol";
5
-
6
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
7
- import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
8
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
9
-
10
- contract RepoFindings is UnitTestSetup {
11
- address payable internal splitBeneficiary = payable(makeAddr("splitBeneficiary"));
12
-
13
- function _payMetadata(
14
- address hookAddress,
15
- bool allowOverspending,
16
- uint16[] memory tierIds
17
- )
18
- internal
19
- view
20
- returns (bytes memory)
21
- {
22
- bytes[] memory data = new bytes[](1);
23
- data[0] = abi.encode(allowOverspending, tierIds);
24
- bytes4[] memory ids = new bytes4[](1);
25
- ids[0] = metadataHelper.getId("pay", hookAddress);
26
- return metadataHelper.createMetadata(ids, data);
27
- }
28
-
29
- function _nativeTokenAmount(uint256 value) internal pure returns (JBTokenAmount memory) {
30
- return JBTokenAmount({
31
- token: JBConstants.NATIVE_TOKEN,
32
- value: value,
33
- decimals: 18,
34
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
35
- });
36
- }
37
-
38
- function test_payCredits_can_underfund_split_bearing_tier_mints() public {
39
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
40
-
41
- vm.mockCall(mockJBSplits, abi.encodeWithSelector(IJBSplits.setSplitGroupsOf.selector), abi.encode());
42
-
43
- JBSplit[] memory tierSplits = new JBSplit[](1);
44
- tierSplits[0] = JBSplit({
45
- preferAddToBalance: false,
46
- percent: JBConstants.SPLITS_TOTAL_PERCENT,
47
- projectId: 0,
48
- beneficiary: splitBeneficiary,
49
- lockedUntil: 0,
50
- hook: IJBSplitHook(address(0))
51
- });
52
-
53
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
54
- tierConfigs[0] = JB721TierConfig({
55
- price: 1 ether,
56
- initialSupply: 10,
57
- votingUnits: 0,
58
- reserveFrequency: 0,
59
- reserveBeneficiary: address(0),
60
- encodedIPFSUri: bytes32(uint256(0x1234)),
61
- category: 1,
62
- discountPercent: 0,
63
- flags: JB721TierConfigFlags({
64
- allowOwnerMint: false,
65
- useReserveBeneficiaryAsDefault: false,
66
- transfersPausable: false,
67
- useVotingUnits: false,
68
- cantBeRemoved: false,
69
- cantIncreaseDiscountPercent: false,
70
- cantBuyWithCredits: false
71
- }),
72
- splitPercent: JBConstants.SPLITS_TOTAL_PERCENT,
73
- splits: tierSplits
74
- });
75
-
76
- vm.prank(owner);
77
- testHook.adjustTiers(tierConfigs, new uint256[](0));
78
-
79
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(1) << 160);
80
- vm.mockCall(
81
- mockJBSplits,
82
- abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, uint256(0), groupId),
83
- abi.encode(tierSplits)
84
- );
85
-
86
- mockAndExpect(
87
- mockJBDirectory,
88
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
89
- abi.encode(true)
90
- );
91
-
92
- vm.prank(mockTerminalAddress);
93
- testHook.afterPayRecordedWith(
94
- JBAfterPayRecordedContext({
95
- payer: beneficiary,
96
- projectId: projectId,
97
- rulesetId: 0,
98
- amount: _nativeTokenAmount(1 ether),
99
- forwardedAmount: _nativeTokenAmount(0),
100
- weight: 10e18,
101
- newlyIssuedTokenCount: 0,
102
- beneficiary: beneficiary,
103
- hookMetadata: bytes(""),
104
- payerMetadata: bytes("")
105
- })
106
- );
107
-
108
- assertEq(testHook.payCreditsOf(beneficiary), 1 ether, "setup: credits should be seeded");
109
-
110
- uint16[] memory tierIdsToMint = new uint16[](1);
111
- tierIdsToMint[0] = 1;
112
- bytes memory payerMetadata = _payMetadata(address(testHook), true, tierIdsToMint);
113
-
114
- JBBeforePayRecordedContext memory beforeContext = JBBeforePayRecordedContext({
115
- terminal: mockTerminalAddress,
116
- payer: beneficiary,
117
- amount: _nativeTokenAmount(1),
118
- projectId: projectId,
119
- rulesetId: 0,
120
- beneficiary: beneficiary,
121
- weight: 10e18,
122
- reservedPercent: 5000,
123
- metadata: payerMetadata
124
- });
125
-
126
- (uint256 weight, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(beforeContext);
127
-
128
- assertEq(weight, 0, "only the fresh 1 wei payment is considered for split weight adjustment");
129
- assertEq(specs.length, 1);
130
- assertEq(specs[0].amount, 1, "split forwarding is capped to the fresh payment, not the credit-backed mint");
131
-
132
- uint256 splitBeneficiaryBalanceBefore = splitBeneficiary.balance;
133
- vm.deal(mockTerminalAddress, 1);
134
-
135
- vm.prank(mockTerminalAddress);
136
- testHook.afterPayRecordedWith{value: 1}(
137
- JBAfterPayRecordedContext({
138
- payer: beneficiary,
139
- projectId: projectId,
140
- rulesetId: 0,
141
- amount: _nativeTokenAmount(1),
142
- forwardedAmount: _nativeTokenAmount(1),
143
- weight: weight,
144
- newlyIssuedTokenCount: 0,
145
- beneficiary: beneficiary,
146
- hookMetadata: specs[0].metadata,
147
- payerMetadata: payerMetadata
148
- })
149
- );
150
-
151
- assertEq(testHook.balanceOf(beneficiary), 1, "beneficiary still receives the split-bearing NFT");
152
- assertEq(testHook.payCreditsOf(beneficiary), 1, "stored credits fund essentially the entire mint");
153
- assertEq(
154
- splitBeneficiary.balance - splitBeneficiaryBalanceBefore,
155
- 1,
156
- "split beneficiary only receives the fresh 1 wei payment instead of the tier's 1 ether split amount"
157
- );
158
- }
159
-
160
- /// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
161
- /// is now rejected at creation time, preventing the retroactive dilution bug.
162
- function test_new_default_reserve_beneficiary_retroactively_dilutes_existing_tiers() public {
163
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
164
-
165
- JB721TierConfig[] memory initialTier = new JB721TierConfig[](1);
166
- initialTier[0] = JB721TierConfig({
167
- price: 1 ether,
168
- initialSupply: 10,
169
- votingUnits: 0,
170
- reserveFrequency: 2,
171
- reserveBeneficiary: address(0),
172
- encodedIPFSUri: bytes32(uint256(0x1111)),
173
- category: 1,
174
- discountPercent: 0,
175
- flags: JB721TierConfigFlags({
176
- allowOwnerMint: false,
177
- useReserveBeneficiaryAsDefault: false,
178
- transfersPausable: false,
179
- useVotingUnits: false,
180
- cantBeRemoved: false,
181
- cantIncreaseDiscountPercent: false,
182
- cantBuyWithCredits: false
183
- }),
184
- splitPercent: 0,
185
- splits: new JBSplit[](0)
186
- });
187
-
188
- // The new creation-time check prevents tiers with reserves but no beneficiary.
189
- vm.expectRevert(
190
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
191
- );
192
- vm.prank(owner);
193
- testHook.adjustTiers(initialTier, new uint256[](0));
194
- }
195
- }
@@ -1,87 +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
- contract Test_ReserveActivation is Test {
13
- JB721TiersHookStore internal store;
14
-
15
- function setUp() external {
16
- store = new JB721TiersHookStore();
17
- }
18
-
19
- /// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
20
- /// is now rejected at creation time. This prevents the phantom-reserves scenario entirely.
21
- function test_soldOutTier_noPhantomReserves_afterDefaultBeneficiaryChange() external {
22
- // Attempt to add a tier with reserve frequency but no beneficiary — should revert.
23
- JB721TierConfig[] memory initialTiers = new JB721TierConfig[](1);
24
- initialTiers[0] = _tier({
25
- price: 1,
26
- initialSupply: 10,
27
- reserveFrequency: 2,
28
- reserveBeneficiary: address(0),
29
- useReserveBeneficiaryAsDefault: false,
30
- category: 1
31
- });
32
-
33
- vm.expectRevert(
34
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
35
- );
36
- store.recordAddTiers(initialTiers);
37
- }
38
-
39
- /// @notice Creating a tier with reserveFrequency > 0 and no beneficiary (tier-specific or default)
40
- /// is now rejected at creation time. This prevents the retroactive reserve activation scenario entirely.
41
- function test_nonSoldOutTier_reservesStillWork_afterDefaultBeneficiaryChange() external {
42
- // Attempt to add a tier with reserve frequency but no beneficiary — should revert.
43
- JB721TierConfig[] memory initialTiers = new JB721TierConfig[](1);
44
- initialTiers[0] = _tier({
45
- price: 1,
46
- initialSupply: 100,
47
- reserveFrequency: 5,
48
- reserveBeneficiary: address(0),
49
- useReserveBeneficiaryAsDefault: false,
50
- category: 1
51
- });
52
-
53
- vm.expectRevert(
54
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_MissingReserveBeneficiary.selector, 1)
55
- );
56
- store.recordAddTiers(initialTiers);
57
- }
58
-
59
- function _tier(
60
- uint104 price,
61
- uint32 initialSupply,
62
- uint16 reserveFrequency,
63
- address reserveBeneficiary,
64
- bool useReserveBeneficiaryAsDefault,
65
- uint24 category
66
- )
67
- internal
68
- pure
69
- returns (JB721TierConfig memory tier)
70
- {
71
- tier.price = price;
72
- tier.initialSupply = initialSupply;
73
- tier.reserveFrequency = reserveFrequency;
74
- tier.reserveBeneficiary = reserveBeneficiary;
75
- tier.category = category;
76
- tier.flags = JB721TierConfigFlags({
77
- allowOwnerMint: false,
78
- useReserveBeneficiaryAsDefault: useReserveBeneficiaryAsDefault,
79
- transfersPausable: false,
80
- useVotingUnits: false,
81
- cantBeRemoved: false,
82
- cantIncreaseDiscountPercent: false,
83
- cantBuyWithCredits: false
84
- });
85
- tier.splits = new JBSplit[](0);
86
- }
87
- }
@@ -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
- }