@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,358 +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
- // forge-lint: disable-next-line(unaliased-plain-import)
7
- import "../../src/JB721TiersHookProjectDeployer.sol";
8
- // forge-lint: disable-next-line(unaliased-plain-import)
9
- import "../../src/structs/JBLaunchRulesetsConfig.sol";
10
- // forge-lint: disable-next-line(unaliased-plain-import)
11
- import "../../src/structs/JBQueueRulesetsConfig.sol";
12
-
13
- import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
14
- import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.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 {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
18
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
19
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
20
-
21
- /// @notice A mock projects contract that returns configurable owner and count.
22
- contract MockProjects {
23
- uint256 private _count;
24
- address private _owner;
25
-
26
- function setup(uint256 initialCount, address projectOwner) external {
27
- _count = initialCount;
28
- _owner = projectOwner;
29
- }
30
-
31
- function count() external view returns (uint256) {
32
- return _count;
33
- }
34
-
35
- function ownerOf(uint256) external view returns (address) {
36
- return _owner;
37
- }
38
- }
39
-
40
- /// @notice A mock controller that records calls and returns a ruleset ID.
41
- contract MockController {
42
- uint256 public lastProjectId;
43
- uint256 public lastRulesetConfigCount;
44
- bool public launchRulesetsForCalled;
45
- bool public queueRulesetsOfCalled;
46
-
47
- receive() external payable {}
48
-
49
- // The fallback accepts any call and returns uint256(42) as the ruleset ID.
50
- fallback() external payable {
51
- // Decode the selector to track which function was called.
52
- bytes4 selector = msg.sig;
53
-
54
- // launchRulesetsFor(uint256,JBRulesetConfig[],JBTerminalConfig[],string)
55
- if (selector == IJBController.launchRulesetsFor.selector) {
56
- launchRulesetsForCalled = true;
57
- }
58
- // queueRulesetsOf(uint256,JBRulesetConfig[],string)
59
- if (selector == IJBController.queueRulesetsOf.selector) {
60
- queueRulesetsOfCalled = true;
61
- }
62
-
63
- // Return uint256(42) as the ruleset ID.
64
- bytes memory result = abi.encode(uint256(42));
65
- assembly {
66
- return(add(result, 32), mload(result))
67
- }
68
- }
69
- }
70
-
71
- /// @notice Regression tests for JB721TiersHookProjectDeployer's launchRulesetsFor and queueRulesetsOf.
72
- /// @dev These verify that the deployer correctly deploys a hook, transfers ownership, wires up the data hook,
73
- /// and delegates to the controller for both launch and queue paths.
74
- contract Test_ProjectDeployerRulesets is UnitTestSetup {
75
- using stdStorage for StdStorage;
76
-
77
- JB721TiersHookProjectDeployer deployer;
78
- MockProjects mockProj;
79
- MockController mockCtrl;
80
-
81
- uint256 testProjectId = 5;
82
-
83
- function setUp() public override {
84
- super.setUp();
85
-
86
- // Deploy mock projects contract and etch onto the existing mockJBProjects address.
87
- mockProj = new MockProjects();
88
- vm.etch(mockJBProjects, address(mockProj).code);
89
- MockProjects(mockJBProjects).setup(testProjectId, owner);
90
-
91
- // Mock DIRECTORY.PROJECTS() to return mockJBProjects.
92
- vm.mockCall(mockJBDirectory, abi.encodeWithSelector(IJBDirectory.PROJECTS.selector), abi.encode(mockJBProjects));
93
-
94
- // Mock all permission checks to return true.
95
- vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(true));
96
-
97
- // Deploy the mock controller.
98
- mockCtrl = new MockController();
99
-
100
- // Deploy the project deployer.
101
- deployer = new JB721TiersHookProjectDeployer(
102
- IJBDirectory(mockJBDirectory), IJBPermissions(mockJBPermissions), jbHookDeployer, address(0)
103
- );
104
- }
105
-
106
- /// @notice Build a minimal deploy config and launch rulesets config for testing.
107
- function _buildLaunchRulesetsConfigs()
108
- internal
109
- view
110
- returns (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig)
111
- {
112
- // Build a minimal tier config (1 tier).
113
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
114
- tierConfigs[0] = JB721TierConfig({
115
- price: uint104(10),
116
- initialSupply: uint32(100),
117
- votingUnits: uint16(0),
118
- reserveFrequency: uint16(0),
119
- reserveBeneficiary: reserveBeneficiary,
120
- encodedIPFSUri: tokenUris[0],
121
- category: uint24(100),
122
- discountPercent: uint8(0),
123
- flags: JB721TierConfigFlags({
124
- allowOwnerMint: false,
125
- useReserveBeneficiaryAsDefault: false,
126
- transfersPausable: false,
127
- useVotingUnits: true,
128
- cantBeRemoved: false,
129
- cantIncreaseDiscountPercent: false,
130
- cantBuyWithCredits: false
131
- }),
132
- splitPercent: 0,
133
- splits: new JBSplit[](0)
134
- });
135
-
136
- hookConfig = JBDeploy721TiersHookConfig({
137
- name: name,
138
- symbol: symbol,
139
- baseUri: baseUri,
140
- tokenUriResolver: IJB721TokenUriResolver(mockTokenUriResolver),
141
- contractUri: contractUri,
142
- tiersConfig: JB721InitTiersConfig({
143
- tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
144
- }),
145
- flags: JB721TiersHookFlags({
146
- preventOverspending: false,
147
- issueTokensForSplits: false,
148
- noNewTiersWithReserves: true,
149
- noNewTiersWithVotes: true,
150
- noNewTiersWithOwnerMinting: true
151
- })
152
- });
153
-
154
- // Build a minimal ruleset config.
155
- JBPayDataHookRulesetConfig[] memory rulesetConfigs = new JBPayDataHookRulesetConfig[](1);
156
- rulesetConfigs[0].mustStartAtOrAfter = 0;
157
- rulesetConfigs[0].duration = 14;
158
- rulesetConfigs[0].weight = 10 ** 18;
159
- rulesetConfigs[0].weightCutPercent = 0;
160
- rulesetConfigs[0].approvalHook = IJBRulesetApprovalHook(address(0));
161
- rulesetConfigs[0].metadata = JBPayDataHookRulesetMetadata({
162
- reservedPercent: 5000,
163
- cashOutTaxRate: 5000,
164
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
165
- pausePay: false,
166
- pauseCreditTransfers: false,
167
- allowOwnerMinting: false,
168
- allowSetCustomToken: false,
169
- allowTerminalMigration: false,
170
- allowSetTerminals: false,
171
- allowSetController: false,
172
- ownerMustSendPayouts: false,
173
- allowAddAccountingContext: false,
174
- allowAddPriceFeed: false,
175
- holdFees: false,
176
- useTotalSurplusForCashOuts: false,
177
- useDataHookForCashOut: false,
178
- metadata: 0x00
179
- });
180
-
181
- JBTerminalConfig[] memory terminalConfigs = new JBTerminalConfig[](1);
182
- JBAccountingContext[] memory accountingContexts = new JBAccountingContext[](1);
183
- accountingContexts[0] = JBAccountingContext({
184
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18, token: JBConstants.NATIVE_TOKEN
185
- });
186
- terminalConfigs[0] = JBTerminalConfig({
187
- terminal: IJBTerminal(mockTerminalAddress), accountingContextsToAccept: accountingContexts
188
- });
189
-
190
- launchConfig = JBLaunchRulesetsConfig({
191
- // forge-lint: disable-next-line(unsafe-typecast)
192
- projectId: uint56(testProjectId),
193
- rulesetConfigurations: rulesetConfigs,
194
- terminalConfigurations: terminalConfigs,
195
- memo: "launch rulesets"
196
- });
197
- }
198
-
199
- /// @notice Build a minimal deploy config and queue rulesets config for testing.
200
- function _buildQueueRulesetsConfigs()
201
- internal
202
- view
203
- returns (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig)
204
- {
205
- // Reuse the launch config builder for the hook config.
206
- JBLaunchRulesetsConfig memory launchConfig;
207
- (hookConfig, launchConfig) = _buildLaunchRulesetsConfigs();
208
-
209
- queueConfig = JBQueueRulesetsConfig({
210
- // forge-lint: disable-next-line(unsafe-typecast)
211
- projectId: uint56(testProjectId),
212
- rulesetConfigurations: launchConfig.rulesetConfigurations,
213
- memo: "queue rulesets"
214
- });
215
- }
216
-
217
- // -----------------------------------------------------------------------
218
- // launchRulesetsFor
219
- // -----------------------------------------------------------------------
220
-
221
- /// @notice launchRulesetsFor deploys a hook, transfers ownership to the project, and calls the controller.
222
- function test_launchRulesetsFor_deploysHookAndCallsController() external {
223
- (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
224
- _buildLaunchRulesetsConfigs();
225
-
226
- // Call as the project owner.
227
- vm.prank(owner);
228
- (uint256 rulesetId, IJB721TiersHook deployedHook) = deployer.launchRulesetsFor(
229
- testProjectId, hookConfig, launchConfig, IJBController(address(mockCtrl)), bytes32(0)
230
- );
231
-
232
- // Verify the ruleset ID returned by the mock controller.
233
- assertEq(rulesetId, 42, "Should return the ruleset ID from the controller");
234
-
235
- // Verify a hook was deployed (non-zero address).
236
- assertTrue(address(deployedHook) != address(0), "Hook should be deployed");
237
-
238
- // Verify the controller's launchRulesetsFor was called.
239
- assertTrue(mockCtrl.launchRulesetsForCalled(), "Controller launchRulesetsFor should be called");
240
-
241
- // Verify the hook's PROJECT_ID is correct.
242
- assertEq(deployedHook.PROJECT_ID(), testProjectId, "Hook PROJECT_ID should match");
243
-
244
- // Verify the hook's ownership was transferred to the project.
245
- (, uint88 ownerProjectId,) = JBOwnable(address(deployedHook)).jbOwner();
246
- assertEq(ownerProjectId, testProjectId, "Hook should be owned by project");
247
- }
248
-
249
- /// @notice launchRulesetsFor with a deterministic salt produces a hook at a predictable address.
250
- function test_launchRulesetsFor_deterministicSalt() external {
251
- (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
252
- _buildLaunchRulesetsConfigs();
253
-
254
- bytes32 salt = bytes32(uint256(0xdead));
255
-
256
- vm.prank(owner);
257
- (, IJB721TiersHook hook1) =
258
- deployer.launchRulesetsFor(testProjectId, hookConfig, launchConfig, IJBController(address(mockCtrl)), salt);
259
-
260
- // Deploy a second hook with a different salt to verify addresses differ.
261
- bytes32 salt2 = bytes32(uint256(0xbeef));
262
-
263
- vm.prank(owner);
264
- (, IJB721TiersHook hook2) =
265
- deployer.launchRulesetsFor(testProjectId, hookConfig, launchConfig, IJBController(address(mockCtrl)), salt2);
266
-
267
- assertTrue(address(hook1) != address(hook2), "Different salts should produce different hook addresses");
268
- }
269
-
270
- /// @notice launchRulesetsFor reverts when the caller lacks QUEUE_RULESETS permission.
271
- function test_launchRulesetsFor_revertsWithoutPermission() external {
272
- (JBDeploy721TiersHookConfig memory hookConfig, JBLaunchRulesetsConfig memory launchConfig) =
273
- _buildLaunchRulesetsConfigs();
274
-
275
- // Override: mock permissions to deny.
276
- vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false));
277
-
278
- address unauthorized = makeAddr("unauthorized");
279
- vm.prank(unauthorized);
280
- vm.expectRevert();
281
- deployer.launchRulesetsFor(
282
- testProjectId, hookConfig, launchConfig, IJBController(address(mockCtrl)), bytes32(0)
283
- );
284
- }
285
-
286
- // -----------------------------------------------------------------------
287
- // queueRulesetsOf
288
- // -----------------------------------------------------------------------
289
-
290
- /// @notice queueRulesetsOf deploys a hook, transfers ownership to the project, and calls the controller.
291
- function test_queueRulesetsOf_deploysHookAndCallsController() external {
292
- (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
293
- _buildQueueRulesetsConfigs();
294
-
295
- vm.prank(owner);
296
- (uint256 rulesetId, IJB721TiersHook deployedHook) = deployer.queueRulesetsOf(
297
- testProjectId, hookConfig, queueConfig, IJBController(address(mockCtrl)), bytes32(0)
298
- );
299
-
300
- // Verify the ruleset ID returned by the mock controller.
301
- assertEq(rulesetId, 42, "Should return the ruleset ID from the controller");
302
-
303
- // Verify a hook was deployed (non-zero address).
304
- assertTrue(address(deployedHook) != address(0), "Hook should be deployed");
305
-
306
- // Verify the controller's queueRulesetsOf was called.
307
- assertTrue(mockCtrl.queueRulesetsOfCalled(), "Controller queueRulesetsOf should be called");
308
-
309
- // Verify the hook's PROJECT_ID is correct.
310
- assertEq(deployedHook.PROJECT_ID(), testProjectId, "Hook PROJECT_ID should match");
311
-
312
- // Verify the hook's ownership was transferred to the project.
313
- (, uint88 ownerProjectId,) = JBOwnable(address(deployedHook)).jbOwner();
314
- assertEq(ownerProjectId, testProjectId, "Hook should be owned by project");
315
- }
316
-
317
- /// @notice queueRulesetsOf correctly wires useDataHookForPay = true in the forwarded ruleset metadata.
318
- function test_queueRulesetsOf_wiresDataHookForPay() external {
319
- (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
320
- _buildQueueRulesetsConfigs();
321
-
322
- vm.prank(owner);
323
- (, IJB721TiersHook deployedHook) = deployer.queueRulesetsOf(
324
- testProjectId, hookConfig, queueConfig, IJBController(address(mockCtrl)), bytes32(0)
325
- );
326
-
327
- // The hook address should not be zero -- this indirectly validates the dataHook was wired.
328
- assertTrue(address(deployedHook) != address(0), "Hook should be deployed and wired as data hook");
329
- }
330
-
331
- /// @notice queueRulesetsOf reverts when the caller lacks QUEUE_RULESETS permission.
332
- function test_queueRulesetsOf_revertsWithoutPermission() external {
333
- (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
334
- _buildQueueRulesetsConfigs();
335
-
336
- // Override: mock permissions to deny.
337
- vm.mockCall(mockJBPermissions, abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false));
338
-
339
- address unauthorized = makeAddr("unauthorized");
340
- vm.prank(unauthorized);
341
- vm.expectRevert();
342
- deployer.queueRulesetsOf(testProjectId, hookConfig, queueConfig, IJBController(address(mockCtrl)), bytes32(0));
343
- }
344
-
345
- /// @notice queueRulesetsOf with a zero salt uses non-deterministic deployment.
346
- function test_queueRulesetsOf_zeroSaltNonDeterministic() external {
347
- (JBDeploy721TiersHookConfig memory hookConfig, JBQueueRulesetsConfig memory queueConfig) =
348
- _buildQueueRulesetsConfigs();
349
-
350
- vm.prank(owner);
351
- (uint256 rulesetId, IJB721TiersHook deployedHook) = deployer.queueRulesetsOf(
352
- testProjectId, hookConfig, queueConfig, IJBController(address(mockCtrl)), bytes32(0)
353
- );
354
-
355
- assertEq(rulesetId, 42, "Should return the ruleset ID");
356
- assertTrue(address(deployedHook) != address(0), "Hook should be deployed");
357
- }
358
- }
@@ -1,155 +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
-
8
- /// @notice defaultReserveBeneficiaryOf is globally overwritten when adding a tier with
9
- /// useReserveBeneficiaryAsDefault=true.
10
- contract Test_L34_ReserveBeneficiaryOverwrite is UnitTestSetup {
11
- using stdStorage for StdStorage;
12
-
13
- address alice = makeAddr("alice");
14
- address bob = makeAddr("bob");
15
-
16
- /// @notice Verify that adding a tier with useReserveBeneficiaryAsDefault=true overwrites the global default
17
- /// and emits the SetDefaultReserveBeneficiary event.
18
- function test_addTierWithDefaultBeneficiary_overwritesGlobal() public {
19
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
20
- IJB721TiersHookStore hookStore = testHook.STORE();
21
-
22
- // Add tier 1: alice as default reserve beneficiary.
23
- JB721TierConfig[] memory tier1Configs = new JB721TierConfig[](1);
24
- tier1Configs[0].price = 1 ether;
25
- tier1Configs[0].initialSupply = uint32(100);
26
- tier1Configs[0].category = uint24(1);
27
- tier1Configs[0].encodedIPFSUri = bytes32(uint256(0x1234));
28
- tier1Configs[0].reserveFrequency = 5;
29
- tier1Configs[0].reserveBeneficiary = alice;
30
- tier1Configs[0].flags.useReserveBeneficiaryAsDefault = true;
31
-
32
- vm.prank(address(testHook));
33
- uint256[] memory tier1Ids = hookStore.recordAddTiers(tier1Configs);
34
-
35
- // Verify alice is the default reserve beneficiary.
36
- assertEq(hookStore.defaultReserveBeneficiaryOf(address(testHook)), alice);
37
-
38
- // Verify tier 1 uses alice as its reserve beneficiary (via the default).
39
- assertEq(hookStore.reserveBeneficiaryOf(address(testHook), tier1Ids[0]), alice);
40
-
41
- // Add tier 2: no useReserveBeneficiaryAsDefault, with a per-tier beneficiary.
42
- JB721TierConfig[] memory tier2Configs = new JB721TierConfig[](1);
43
- tier2Configs[0].price = 2 ether;
44
- tier2Configs[0].initialSupply = uint32(100);
45
- tier2Configs[0].category = uint24(2);
46
- tier2Configs[0].encodedIPFSUri = bytes32(uint256(0x5678));
47
- tier2Configs[0].reserveFrequency = 5;
48
- tier2Configs[0].reserveBeneficiary = bob;
49
- tier2Configs[0].flags.useReserveBeneficiaryAsDefault = false;
50
-
51
- vm.prank(address(testHook));
52
- uint256[] memory tier2Ids = hookStore.recordAddTiers(tier2Configs);
53
-
54
- // Default should still be alice.
55
- assertEq(hookStore.defaultReserveBeneficiaryOf(address(testHook)), alice);
56
-
57
- // Tier 2 should use bob (tier-specific).
58
- assertEq(hookStore.reserveBeneficiaryOf(address(testHook), tier2Ids[0]), bob);
59
-
60
- // Now add tier 3: bob as the NEW default reserve beneficiary.
61
- // This should overwrite the default, affecting tier 1 which relies on the default.
62
- JB721TierConfig[] memory tier3Configs = new JB721TierConfig[](1);
63
- tier3Configs[0].price = 3 ether;
64
- tier3Configs[0].initialSupply = uint32(100);
65
- tier3Configs[0].category = uint24(3);
66
- tier3Configs[0].encodedIPFSUri = bytes32(uint256(0x9ABC));
67
- tier3Configs[0].reserveFrequency = 5;
68
- tier3Configs[0].reserveBeneficiary = bob;
69
- tier3Configs[0].flags.useReserveBeneficiaryAsDefault = true;
70
-
71
- vm.prank(address(testHook));
72
- uint256[] memory tier3Ids = hookStore.recordAddTiers(tier3Configs);
73
-
74
- // Default should now be bob.
75
- assertEq(hookStore.defaultReserveBeneficiaryOf(address(testHook)), bob);
76
-
77
- // Tier 1 should now resolve to bob (the new default), NOT alice.
78
- // This is the documented global overwrite behavior.
79
- assertEq(hookStore.reserveBeneficiaryOf(address(testHook), tier1Ids[0]), bob);
80
-
81
- // Tier 2 should still use bob (tier-specific, unaffected by default change).
82
- assertEq(hookStore.reserveBeneficiaryOf(address(testHook), tier2Ids[0]), bob);
83
-
84
- // Tier 3 should also resolve to bob (via the default).
85
- assertEq(hookStore.reserveBeneficiaryOf(address(testHook), tier3Ids[0]), bob);
86
- }
87
-
88
- /// @notice Verify that the SetDefaultReserveBeneficiary event is emitted when the default changes.
89
- function test_addTierWithDefaultBeneficiary_emitsEvent() public {
90
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
91
- IJB721TiersHookStore hookStore = testHook.STORE();
92
-
93
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
94
- tierConfigs[0].price = 1 ether;
95
- tierConfigs[0].initialSupply = uint32(100);
96
- tierConfigs[0].category = uint24(1);
97
- tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
98
- tierConfigs[0].reserveFrequency = 5;
99
- tierConfigs[0].reserveBeneficiary = alice;
100
- tierConfigs[0].flags.useReserveBeneficiaryAsDefault = true;
101
-
102
- // Expect the SetDefaultReserveBeneficiary event.
103
- vm.expectEmit(true, true, false, true, address(hookStore));
104
- emit IJB721TiersHookStore.SetDefaultReserveBeneficiary({
105
- hook: address(testHook), newBeneficiary: alice, caller: address(testHook)
106
- });
107
-
108
- vm.prank(address(testHook));
109
- hookStore.recordAddTiers(tierConfigs);
110
- }
111
-
112
- /// @notice Verify that no event is emitted when the beneficiary is already the same.
113
- function test_addTierWithSameDefaultBeneficiary_noEvent() public {
114
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
115
- IJB721TiersHookStore hookStore = testHook.STORE();
116
-
117
- // First, set alice as the default.
118
- JB721TierConfig[] memory tier1Configs = new JB721TierConfig[](1);
119
- tier1Configs[0].price = 1 ether;
120
- tier1Configs[0].initialSupply = uint32(100);
121
- tier1Configs[0].category = uint24(1);
122
- tier1Configs[0].encodedIPFSUri = bytes32(uint256(0x1234));
123
- tier1Configs[0].reserveFrequency = 5;
124
- tier1Configs[0].reserveBeneficiary = alice;
125
- tier1Configs[0].flags.useReserveBeneficiaryAsDefault = true;
126
-
127
- vm.prank(address(testHook));
128
- hookStore.recordAddTiers(tier1Configs);
129
-
130
- // Now add another tier with the same default — no event should be emitted.
131
- JB721TierConfig[] memory tier2Configs = new JB721TierConfig[](1);
132
- tier2Configs[0].price = 2 ether;
133
- tier2Configs[0].initialSupply = uint32(100);
134
- tier2Configs[0].category = uint24(2);
135
- tier2Configs[0].encodedIPFSUri = bytes32(uint256(0x5678));
136
- tier2Configs[0].reserveFrequency = 5;
137
- tier2Configs[0].reserveBeneficiary = alice;
138
- tier2Configs[0].flags.useReserveBeneficiaryAsDefault = true;
139
-
140
- // Record the logs to verify no SetDefaultReserveBeneficiary event is emitted.
141
- vm.recordLogs();
142
-
143
- vm.prank(address(testHook));
144
- hookStore.recordAddTiers(tier2Configs);
145
-
146
- Vm.Log[] memory logs = vm.getRecordedLogs();
147
- for (uint256 i; i < logs.length; i++) {
148
- // SetDefaultReserveBeneficiary event signature.
149
- assertTrue(
150
- logs[i].topics[0] != keccak256("SetDefaultReserveBeneficiary(address,address,address)"),
151
- "SetDefaultReserveBeneficiary should not be emitted when beneficiary unchanged"
152
- );
153
- }
154
- }
155
- }