@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,1661 +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
-
7
- contract Test_afterPayRecorded_Unit is UnitTestSetup {
8
- using stdStorage for StdStorage;
9
-
10
- function test_afterPayRecorded_mintAndReserveCorrectAmounts(
11
- uint256 initialSupply,
12
- uint256 nftsToMint,
13
- uint256 reserveFrequency
14
- )
15
- public
16
- {
17
- initialSupply = 400;
18
- reserveFrequency = bound(reserveFrequency, 0, 200);
19
- nftsToMint = bound(nftsToMint, 1, 200);
20
-
21
- // forge-lint: disable-next-line(unsafe-typecast)
22
- defaultTierConfig.initialSupply = uint32(initialSupply);
23
- // forge-lint: disable-next-line(unsafe-typecast)
24
- defaultTierConfig.reserveFrequency = uint16(reserveFrequency);
25
- ForTest_JB721TiersHook hook = _initializeForTestHook(1); // Initialize with 1 default tier.
26
-
27
- // Mock the directory call.
28
- mockAndExpect(
29
- address(mockJBDirectory),
30
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
31
- abi.encode(true)
32
- );
33
-
34
- uint16[] memory tierIdsToMint = new uint16[](nftsToMint);
35
-
36
- for (uint256 i; i < nftsToMint; i++) {
37
- tierIdsToMint[i] = uint16(1);
38
- }
39
-
40
- // Build the metadata using the tiers to mint and the overspending flag.
41
- bytes[] memory data = new bytes[](1);
42
- data[0] = abi.encode(false, tierIdsToMint);
43
-
44
- // Pass the hook ID.
45
- bytes4[] memory ids = new bytes4[](1);
46
- ids[0] = metadataHelper.getId("pay", address(hook));
47
-
48
- // Generate the metadata.
49
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
50
-
51
- JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
52
- payer: beneficiary,
53
- projectId: projectId,
54
- rulesetId: 0,
55
- amount: JBTokenAmount({
56
- token: JBConstants.NATIVE_TOKEN,
57
- value: 10 * nftsToMint,
58
- decimals: 18,
59
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
60
- }),
61
- forwardedAmount: JBTokenAmount({
62
- token: JBConstants.NATIVE_TOKEN,
63
- value: 0,
64
- decimals: 18,
65
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
66
- }), // 0,
67
- // forwarded to the hook.
68
- weight: 10 ** 18,
69
- newlyIssuedTokenCount: 0,
70
- beneficiary: beneficiary,
71
- hookMetadata: bytes(""),
72
- payerMetadata: hookMetadata
73
- });
74
-
75
- vm.prank(mockTerminalAddress);
76
- hook.afterPayRecordedWith(payContext);
77
-
78
- // Check: has the correct number of NFTs been minted for the beneficiary?
79
- assertEq(hook.balanceOf(beneficiary), nftsToMint);
80
-
81
- // Check: were the correct number of NFTs reserved?
82
- if (reserveFrequency > 0 && initialSupply - nftsToMint > 0) {
83
- uint256 reservedToken = nftsToMint / reserveFrequency;
84
- if (nftsToMint % reserveFrequency > 0) reservedToken += 1;
85
-
86
- assertEq(hook.STORE().numberOfPendingReservesFor(address(hook), 1), reservedToken);
87
-
88
- // Mint the pending reserves for the beneficiary.
89
- vm.prank(owner);
90
- hook.mintPendingReservesFor(1, reservedToken);
91
-
92
- // Check: did the reserve beneficiary receive the correct number of NFTs?
93
- assertEq(hook.balanceOf(reserveBeneficiary), reservedToken);
94
- } else {
95
- // Check: does the reserve beneficiary have no NFTs?
96
- assertEq(hook.balanceOf(reserveBeneficiary), 0);
97
- }
98
- }
99
-
100
- // If the amount paid is less than the NFT's price, the payment should revert if overspending is not allowed and no
101
- // metadata was passed.
102
- function test_afterPayRecorded_revertsOnAmountBelowPriceIfNoMetadataAndOverspendingIsPrevented() public {
103
- JB721TiersHook hook = _initHookDefaultTiers(10, true);
104
-
105
- // Mock the directory call.
106
- mockAndExpect(
107
- address(mockJBDirectory),
108
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
109
- abi.encode(true)
110
- );
111
-
112
- // Expect a revert for overspending.
113
- vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_Overspending.selector, tiers[0].price - 1));
114
-
115
- vm.prank(mockTerminalAddress);
116
- hook.afterPayRecordedWith(
117
- JBAfterPayRecordedContext({
118
- payer: msg.sender,
119
- projectId: projectId,
120
- rulesetId: 0,
121
- // 1 wei below the minimum amount
122
- amount: JBTokenAmount({
123
- token: JBConstants.NATIVE_TOKEN,
124
- value: tiers[0].price - 1,
125
- decimals: 18,
126
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
127
- }),
128
- forwardedAmount: JBTokenAmount({
129
- token: JBConstants.NATIVE_TOKEN,
130
- value: 0,
131
- decimals: 18,
132
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
133
- }), // 0,
134
- // forwarded to the hook.
135
- weight: 10 ** 18,
136
- newlyIssuedTokenCount: 0,
137
- beneficiary: msg.sender,
138
- hookMetadata: new bytes(0),
139
- payerMetadata: new bytes(0)
140
- })
141
- );
142
- }
143
-
144
- // If the amount paid is less than the NFT's price, the payment should not revert if overspending is allowed and no
145
- // metadata was passed.
146
- function test_afterPayRecorded_doesNotRevertOnAmountBelowPriceIfNoMetadata() public {
147
- // Mock the directory call.
148
- mockAndExpect(
149
- address(mockJBDirectory),
150
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
151
- abi.encode(true)
152
- );
153
-
154
- vm.prank(mockTerminalAddress);
155
- hook.afterPayRecordedWith(
156
- JBAfterPayRecordedContext({
157
- payer: msg.sender,
158
- projectId: projectId,
159
- rulesetId: 0,
160
- // 1 wei below the minimum amount
161
- amount: JBTokenAmount({
162
- token: JBConstants.NATIVE_TOKEN,
163
- value: tiers[0].price - 1,
164
- decimals: 18,
165
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
166
- }),
167
- forwardedAmount: JBTokenAmount({
168
- token: JBConstants.NATIVE_TOKEN,
169
- value: 0,
170
- decimals: 18,
171
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
172
- }), // 0,
173
- // forwarded to the hook.
174
- weight: 10 ** 18,
175
- newlyIssuedTokenCount: 0,
176
- beneficiary: msg.sender,
177
- hookMetadata: new bytes(0),
178
- payerMetadata: new bytes(0)
179
- })
180
- );
181
-
182
- // Check: does the payer have the correct number of pay credits?
183
- assertEq(hook.payCreditsOf(msg.sender), tiers[0].price - 1);
184
- }
185
-
186
- // If a tier is passed and the amount paid exceeds that NFT's price, mint as many NFTs as possible.
187
- function test_afterPayRecorded_mintCorrectTier() public {
188
- // Mock the directory call.
189
- mockAndExpect(
190
- address(mockJBDirectory),
191
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
192
- abi.encode(true)
193
- );
194
-
195
- uint256 totalSupplyBeforePay = hook.STORE().totalSupplyOf(address(hook));
196
-
197
- bool allowOverspending;
198
- uint16[] memory tierIdsToMint = new uint16[](3);
199
- tierIdsToMint[0] = 1;
200
- tierIdsToMint[1] = 1;
201
- tierIdsToMint[2] = 2;
202
-
203
- // Build the metadata using the tiers to mint and the overspending flag.
204
- bytes[] memory data = new bytes[](1);
205
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
206
-
207
- // Pass the hook ID.
208
- bytes4[] memory ids = new bytes4[](1);
209
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
210
-
211
- // Generate the metadata.
212
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
213
- vm.prank(mockTerminalAddress);
214
- hook.afterPayRecordedWith(
215
- JBAfterPayRecordedContext({
216
- payer: msg.sender,
217
- projectId: projectId,
218
- rulesetId: 0,
219
- amount: JBTokenAmount({
220
- token: JBConstants.NATIVE_TOKEN,
221
- value: tiers[0].price * 2 + tiers[1].price,
222
- decimals: 18,
223
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
224
- }),
225
- forwardedAmount: JBTokenAmount({
226
- token: JBConstants.NATIVE_TOKEN,
227
- value: 0,
228
- decimals: 18,
229
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
230
- }), // 0,
231
- // forwarded to the hook.
232
- weight: 10 ** 18,
233
- newlyIssuedTokenCount: 0,
234
- beneficiary: msg.sender,
235
- hookMetadata: new bytes(0),
236
- payerMetadata: hookMetadata
237
- })
238
- );
239
-
240
- // Check: has the correct number of NFTs been minted?
241
- assertEq(totalSupplyBeforePay + 3, hook.STORE().totalSupplyOf(address(hook)));
242
-
243
- // Check: has the correct number of NFTs been minted in each tier?
244
- assertEq(hook.ownerOf(_generateTokenId(1, 1)), msg.sender);
245
- assertEq(hook.ownerOf(_generateTokenId(1, 2)), msg.sender);
246
- assertEq(hook.ownerOf(_generateTokenId(2, 1)), msg.sender);
247
- }
248
-
249
- // If no tiers are passed, no NFTs should be minted.
250
- function test_afterPayRecorded_mintNoneIfNonePassed(uint8 amount) public {
251
- // Mock the directory call.
252
- mockAndExpect(
253
- address(mockJBDirectory),
254
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
255
- abi.encode(true)
256
- );
257
-
258
- uint256 totalSupplyBeforePay = hook.STORE().totalSupplyOf(address(hook));
259
-
260
- bool allowOverspending = true;
261
- uint16[] memory tierIdsToMint = new uint16[](0);
262
- bytes memory metadata =
263
- abi.encode(bytes32(0), bytes32(0), type(IJB721TiersHook).interfaceId, allowOverspending, tierIdsToMint);
264
-
265
- vm.prank(mockTerminalAddress);
266
- hook.afterPayRecordedWith(
267
- JBAfterPayRecordedContext({
268
- payer: msg.sender,
269
- projectId: projectId,
270
- rulesetId: 0,
271
- amount: JBTokenAmount({
272
- token: JBConstants.NATIVE_TOKEN,
273
- value: amount,
274
- decimals: 18,
275
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
276
- }),
277
- forwardedAmount: JBTokenAmount({
278
- token: JBConstants.NATIVE_TOKEN,
279
- value: 0,
280
- decimals: 18,
281
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
282
- }), // 0,
283
- // forwarded to the hook.
284
- weight: 10 ** 18,
285
- newlyIssuedTokenCount: 0,
286
- beneficiary: msg.sender,
287
- hookMetadata: new bytes(0),
288
- payerMetadata: metadata
289
- })
290
- );
291
-
292
- // Check: has the total supply stayed the same?
293
- assertEq(totalSupplyBeforePay, hook.STORE().totalSupplyOf(address(hook)));
294
- }
295
-
296
- function test_afterPayRecorded_mintTierAndTrackLeftover() public {
297
- uint256 leftover = tiers[0].price - 1;
298
- uint256 amount = tiers[0].price + leftover;
299
-
300
- // Mock the directory call.
301
- mockAndExpect(
302
- address(mockJBDirectory),
303
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
304
- abi.encode(true)
305
- );
306
-
307
- bool allowOverspending = true;
308
- uint16[] memory tierIdsToMint = new uint16[](1);
309
- tierIdsToMint[0] = uint16(1);
310
-
311
- // Build the metadata using the tiers to mint and the overspending flag.
312
- bytes[] memory data = new bytes[](1);
313
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
314
-
315
- // Pass the hook ID.
316
- bytes4[] memory ids = new bytes4[](1);
317
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
318
-
319
- // Generate the metadata.
320
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
321
-
322
- // Calculate the new pay credits.
323
- uint256 newPayCredits = leftover + hook.payCreditsOf(beneficiary);
324
-
325
- vm.expectEmit(true, true, true, true, address(hook));
326
- emit AddPayCredits(newPayCredits, newPayCredits, beneficiary, mockTerminalAddress);
327
-
328
- vm.prank(mockTerminalAddress);
329
- hook.afterPayRecordedWith(
330
- JBAfterPayRecordedContext({
331
- payer: msg.sender,
332
- projectId: projectId,
333
- rulesetId: 0,
334
- amount: JBTokenAmount({
335
- token: JBConstants.NATIVE_TOKEN,
336
- value: amount,
337
- decimals: 18,
338
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
339
- }),
340
- forwardedAmount: JBTokenAmount({
341
- token: JBConstants.NATIVE_TOKEN,
342
- value: 0,
343
- decimals: 18,
344
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
345
- }), // 0,
346
- // forwarded to the hook.
347
- weight: 10 ** 18,
348
- newlyIssuedTokenCount: 0,
349
- beneficiary: beneficiary,
350
- hookMetadata: new bytes(0),
351
- payerMetadata: hookMetadata
352
- })
353
- );
354
-
355
- // Check: has the pay credit balance been updated appropriately?
356
- assertEq(hook.payCreditsOf(beneficiary), leftover);
357
- }
358
-
359
- // Mint various tiers, leaving leftovers, and use the resulting pay credits to mint more NFTs.
360
- function test_afterPayRecorded_mintCorrectTiersWhenPartiallyUsingPayCredits() public {
361
- uint256 leftover = tiers[0].price + 1; // + 1 to avoid rounding error
362
- uint256 amount = tiers[0].price * 2 + tiers[1].price + leftover / 2;
363
-
364
- // Mock the directory call.
365
- mockAndExpect(
366
- address(mockJBDirectory),
367
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
368
- abi.encode(true)
369
- );
370
-
371
- bool allowOverspending = true;
372
- uint16[] memory tierIdsToMint = new uint16[](3);
373
- tierIdsToMint[0] = 1;
374
- tierIdsToMint[1] = 1;
375
- tierIdsToMint[2] = 2;
376
-
377
- // Build the metadata using the tiers to mint and the overspending flag.
378
- bytes[] memory data = new bytes[](1);
379
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
380
-
381
- // Pass the hook ID.
382
- bytes4[] memory ids = new bytes4[](1);
383
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
384
-
385
- // Generate the metadata.
386
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
387
-
388
- uint256 payCredits = hook.payCreditsOf(beneficiary);
389
-
390
- leftover = leftover / 2 + payCredits; // Amount left over.
391
-
392
- vm.expectEmit(true, true, true, true, address(hook));
393
- emit AddPayCredits(leftover - payCredits, leftover, beneficiary, mockTerminalAddress);
394
-
395
- // First call will mint the 3 tiers requested and accumulate half of the first price in pay credits.
396
- vm.prank(mockTerminalAddress);
397
- hook.afterPayRecordedWith(
398
- JBAfterPayRecordedContext({
399
- payer: beneficiary,
400
- projectId: projectId,
401
- rulesetId: 0,
402
- amount: JBTokenAmount({
403
- token: JBConstants.NATIVE_TOKEN,
404
- value: amount,
405
- decimals: 18,
406
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
407
- }),
408
- forwardedAmount: JBTokenAmount({
409
- token: JBConstants.NATIVE_TOKEN,
410
- value: 0,
411
- decimals: 18,
412
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
413
- }), // 0,
414
- // forwarded to the hook.
415
- weight: 10 ** 18,
416
- newlyIssuedTokenCount: 0,
417
- beneficiary: beneficiary,
418
- hookMetadata: new bytes(0),
419
- payerMetadata: hookMetadata
420
- })
421
- );
422
-
423
- uint256 totalSupplyBefore = hook.STORE().totalSupplyOf(address(hook));
424
- {
425
- // We now attempt to mint an additional NFT from tier 1 using the pay credits we collected.
426
- uint16[] memory moreTierIdsToMint = new uint16[](4);
427
- moreTierIdsToMint[0] = 1;
428
- moreTierIdsToMint[1] = 1;
429
- moreTierIdsToMint[2] = 2;
430
- moreTierIdsToMint[3] = 1;
431
-
432
- data[0] = abi.encode(allowOverspending, moreTierIdsToMint);
433
-
434
- // Generate the metadata.
435
- hookMetadata = metadataHelper.createMetadata(ids, data);
436
- }
437
-
438
- // Fetch existing credits.
439
- payCredits = hook.payCreditsOf(beneficiary);
440
- vm.expectEmit(true, true, true, true, address(hook));
441
- emit UsePayCredits(
442
- payCredits,
443
- 0, // No stashed credits.
444
- beneficiary,
445
- mockTerminalAddress
446
- );
447
-
448
- // Second call will mint another 3 tiers requested and mint from the first tier using pay credits.
449
- vm.prank(mockTerminalAddress);
450
- hook.afterPayRecordedWith(
451
- JBAfterPayRecordedContext({
452
- payer: beneficiary,
453
- projectId: projectId,
454
- rulesetId: 0,
455
- amount: JBTokenAmount({
456
- token: JBConstants.NATIVE_TOKEN,
457
- value: amount,
458
- decimals: 18,
459
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
460
- }),
461
- forwardedAmount: JBTokenAmount({
462
- token: JBConstants.NATIVE_TOKEN,
463
- value: 0,
464
- decimals: 18,
465
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
466
- }), // 0,
467
- // forwarded to the hook.
468
- weight: 10 ** 18,
469
- newlyIssuedTokenCount: 0,
470
- beneficiary: beneficiary,
471
- hookMetadata: new bytes(0),
472
- payerMetadata: hookMetadata
473
- })
474
- );
475
-
476
- // Check: has the total supply increased?
477
- assertEq(totalSupplyBefore + 4, hook.STORE().totalSupplyOf(address(hook)));
478
-
479
- // Check: have the correct tiers been minted...
480
- // ... from the first payment?
481
- assertEq(hook.ownerOf(_generateTokenId(1, 1)), beneficiary);
482
- assertEq(hook.ownerOf(_generateTokenId(1, 2)), beneficiary);
483
- assertEq(hook.ownerOf(_generateTokenId(2, 1)), beneficiary);
484
-
485
- // ... from the second payment?
486
- assertEq(hook.ownerOf(_generateTokenId(1, 3)), beneficiary);
487
- assertEq(hook.ownerOf(_generateTokenId(1, 4)), beneficiary);
488
- assertEq(hook.ownerOf(_generateTokenId(1, 5)), beneficiary);
489
- assertEq(hook.ownerOf(_generateTokenId(2, 2)), beneficiary);
490
-
491
- // Check: have all pay credits been used?
492
- assertEq(hook.payCreditsOf(beneficiary), 0);
493
- }
494
-
495
- function test_afterPayRecorded_doNotMintWithSomeoneElsesCredits() public {
496
- uint256 leftover = tiers[0].price + 1; // + 1 to avoid rounding error.
497
- uint256 amount = tiers[0].price * 2 + tiers[1].price + leftover / 2;
498
-
499
- // Mock the directory call.
500
- mockAndExpect(
501
- address(mockJBDirectory),
502
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
503
- abi.encode(true)
504
- );
505
-
506
- bool allowOverspending = true;
507
- uint16[] memory tierIdsToMint = new uint16[](3);
508
- tierIdsToMint[0] = 1;
509
- tierIdsToMint[1] = 1;
510
- tierIdsToMint[2] = 2;
511
-
512
- // Build the metadata using the tiers to mint and the overspending flag.
513
- bytes[] memory data = new bytes[](1);
514
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
515
-
516
- // Pass the hook ID.
517
- bytes4[] memory ids = new bytes4[](1);
518
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
519
-
520
- // Generate the metadata.
521
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
522
-
523
- // The first call will mint the 3 tiers requested and accumulate half of the first price as pay credits.
524
- vm.prank(mockTerminalAddress);
525
- hook.afterPayRecordedWith(
526
- JBAfterPayRecordedContext({
527
- payer: beneficiary,
528
- projectId: projectId,
529
- rulesetId: 0,
530
- amount: JBTokenAmount({
531
- token: JBConstants.NATIVE_TOKEN,
532
- value: amount,
533
- decimals: 18,
534
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
535
- }),
536
- forwardedAmount: JBTokenAmount({
537
- token: JBConstants.NATIVE_TOKEN,
538
- value: 0,
539
- decimals: 18,
540
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
541
- }), // 0,
542
- // forwarded to the hook.
543
- weight: 10 ** 18,
544
- newlyIssuedTokenCount: 0,
545
- beneficiary: beneficiary,
546
- hookMetadata: new bytes(0),
547
- payerMetadata: hookMetadata
548
- })
549
- );
550
-
551
- uint256 totalSupplyBefore = hook.STORE().totalSupplyOf(address(hook));
552
- uint256 payCreditsBefore = hook.payCreditsOf(beneficiary);
553
-
554
- // The second call will mint another 3 tiers requested but NOT with the pay credits.
555
- vm.prank(mockTerminalAddress);
556
- hook.afterPayRecordedWith(
557
- JBAfterPayRecordedContext({
558
- payer: msg.sender,
559
- projectId: projectId,
560
- rulesetId: 0,
561
- amount: JBTokenAmount({
562
- token: JBConstants.NATIVE_TOKEN,
563
- value: amount,
564
- decimals: 18,
565
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
566
- }),
567
- forwardedAmount: JBTokenAmount({
568
- token: JBConstants.NATIVE_TOKEN,
569
- value: 0,
570
- decimals: 18,
571
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
572
- }), // 0,
573
- // forwarded to the hook.
574
- weight: 10 ** 18,
575
- newlyIssuedTokenCount: 0,
576
- beneficiary: beneficiary,
577
- hookMetadata: new bytes(0),
578
- payerMetadata: hookMetadata
579
- })
580
- );
581
-
582
- // Check: has the total supply has increased by 3 NFTs?
583
- assertEq(totalSupplyBefore + 3, hook.STORE().totalSupplyOf(address(hook)));
584
-
585
- // Check: were the correct tiers minted...
586
- // ... from the first payment?
587
- assertEq(hook.ownerOf(_generateTokenId(1, 1)), beneficiary);
588
- assertEq(hook.ownerOf(_generateTokenId(1, 2)), beneficiary);
589
- assertEq(hook.ownerOf(_generateTokenId(2, 1)), beneficiary);
590
-
591
- // ... from the second payment (without extras from the pay credits)?
592
- assertEq(hook.ownerOf(_generateTokenId(1, 3)), beneficiary);
593
- assertEq(hook.ownerOf(_generateTokenId(1, 4)), beneficiary);
594
- assertEq(hook.ownerOf(_generateTokenId(2, 2)), beneficiary);
595
-
596
- // Check: are pay credits from both payments left over?
597
- assertEq(hook.payCreditsOf(beneficiary), payCreditsBefore * 2);
598
- }
599
-
600
- // The terminal uses currency 1 with 18 decimals, and the hook uses currency 2 with 9 decimals.
601
- // The conversion rate is set at 1:2.
602
- function test_afterPayRecorded_mintCorrectTierWithAnotherCurrency() public {
603
- // Etch code onto mockJBPrices so it can receive calls (PRICES is immutable from the constructor).
604
- vm.etch(mockJBPrices, new bytes(1));
605
-
606
- // Currency 2, with 9 decimals.
607
- JB721TiersHook hook = _initHookDefaultTiers(10, false, 2, 9);
608
-
609
- // Mock the directory call.
610
- mockAndExpect(
611
- address(mockJBDirectory),
612
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
613
- abi.encode(true)
614
- );
615
-
616
- // Mock the price oracle call on the hook's actual PRICES address.
617
- uint256 amountInEth = (tiers[0].price * 2 + tiers[1].price) * 2;
618
- mockAndExpect(
619
- mockJBPrices,
620
- abi.encodeCall(IJBPrices.pricePerUnitOf, (projectId, uint32(uint160(JBConstants.NATIVE_TOKEN)), 2, 18)),
621
- abi.encode(2 * 10 ** 9)
622
- );
623
-
624
- uint256 totalSupplyBeforePay = hook.STORE().totalSupplyOf(address(hook));
625
-
626
- bool allowOverspending = true;
627
- uint16[] memory tierIdsToMint = new uint16[](3);
628
- tierIdsToMint[0] = 1;
629
- tierIdsToMint[1] = 1;
630
- tierIdsToMint[2] = 2;
631
-
632
- // Build the metadata using the tiers to mint and the overspending flag.
633
- bytes[] memory data = new bytes[](1);
634
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
635
-
636
- // Pass the hook ID.
637
- bytes4[] memory ids = new bytes4[](1);
638
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
639
-
640
- // Generate the metadata.
641
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
642
-
643
- vm.prank(mockTerminalAddress);
644
- hook.afterPayRecordedWith(
645
- JBAfterPayRecordedContext({
646
- payer: msg.sender,
647
- projectId: projectId,
648
- rulesetId: 0,
649
- amount: JBTokenAmount({
650
- token: JBConstants.NATIVE_TOKEN,
651
- value: amountInEth,
652
- decimals: 18,
653
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
654
- }),
655
- forwardedAmount: JBTokenAmount({
656
- token: JBConstants.NATIVE_TOKEN,
657
- value: 0,
658
- decimals: 18,
659
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
660
- }), // 0,
661
- // forwarded to the hook.
662
- weight: 10 ** 18,
663
- newlyIssuedTokenCount: 0,
664
- beneficiary: msg.sender,
665
- hookMetadata: new bytes(0),
666
- payerMetadata: hookMetadata
667
- })
668
- );
669
-
670
- // Make sure 3 new NFTs were minted.
671
- assertEq(totalSupplyBeforePay + 3, hook.STORE().totalSupplyOf(address(hook)));
672
-
673
- // Check: have the correct NFT tiers been minted?
674
- assertEq(hook.ownerOf(_generateTokenId(1, 1)), msg.sender);
675
- assertEq(hook.ownerOf(_generateTokenId(1, 2)), msg.sender);
676
- assertEq(hook.ownerOf(_generateTokenId(2, 1)), msg.sender);
677
- }
678
-
679
- // If the tier has been removed, revert.
680
- function test_afterPayRecorded_revertIfTierRemoved() public {
681
- // Mock the directory call.
682
- mockAndExpect(
683
- address(mockJBDirectory),
684
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
685
- abi.encode(true)
686
- );
687
-
688
- uint256 totalSupplyBeforePay = hook.STORE().totalSupplyOf(address(hook));
689
-
690
- bool allowOverspending;
691
- uint16[] memory tierIdsToMint = new uint16[](3);
692
- tierIdsToMint[0] = 1;
693
- tierIdsToMint[1] = 1;
694
- tierIdsToMint[2] = 2;
695
-
696
- // Build the metadata using the tiers to mint and the overspending flag.
697
- bytes[] memory data = new bytes[](1);
698
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
699
-
700
- // Pass the hook ID.
701
- bytes4[] memory ids = new bytes4[](1);
702
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
703
-
704
- // Generate the metadata.
705
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
706
-
707
- uint256[] memory toRemove = new uint256[](1);
708
- toRemove[0] = 1;
709
-
710
- vm.prank(owner);
711
- hook.adjustTiers(new JB721TierConfig[](0), toRemove);
712
-
713
- vm.expectRevert(abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_TierRemoved.selector, 1));
714
-
715
- vm.prank(mockTerminalAddress);
716
- hook.afterPayRecordedWith(
717
- JBAfterPayRecordedContext({
718
- payer: msg.sender,
719
- projectId: projectId,
720
- rulesetId: 0,
721
- amount: JBTokenAmount({
722
- token: JBConstants.NATIVE_TOKEN,
723
- value: tiers[0].price * 2 + tiers[1].price,
724
- decimals: 18,
725
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
726
- }),
727
- forwardedAmount: JBTokenAmount({
728
- token: JBConstants.NATIVE_TOKEN,
729
- value: 0,
730
- decimals: 18,
731
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
732
- }), // 0,
733
- // forwarded to the hook.
734
- weight: 10 ** 18,
735
- newlyIssuedTokenCount: 0,
736
- beneficiary: msg.sender,
737
- hookMetadata: new bytes(0),
738
- payerMetadata: hookMetadata
739
- })
740
- );
741
-
742
- // Check: has the total supply stayed the same?
743
- assertEq(totalSupplyBeforePay, hook.STORE().totalSupplyOf(address(hook)));
744
- }
745
-
746
- function test_afterPayRecorded_revertIfTierDoesNotExist(uint256 invalidTier) public {
747
- invalidTier = bound(invalidTier, tiers.length + 1, type(uint16).max);
748
-
749
- // Mock the directory call.
750
- mockAndExpect(
751
- address(mockJBDirectory),
752
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
753
- abi.encode(true)
754
- );
755
-
756
- uint256 totalSupplyBeforePay = hook.STORE().totalSupplyOf(address(hook));
757
-
758
- bool allowOverspending;
759
- uint16[] memory tierIdsToMint = new uint16[](1);
760
- // forge-lint: disable-next-line(unsafe-typecast)
761
- tierIdsToMint[0] = uint16(invalidTier);
762
-
763
- // Build the metadata using the tiers to mint and the overspending flag.
764
- bytes[] memory data = new bytes[](1);
765
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
766
-
767
- // Pass the hook ID.
768
- bytes4[] memory ids = new bytes4[](1);
769
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
770
-
771
- // Generate the metadata.
772
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
773
-
774
- uint256[] memory toRemove = new uint256[](1);
775
- toRemove[0] = 1;
776
-
777
- vm.prank(owner);
778
- hook.adjustTiers(new JB721TierConfig[](0), toRemove);
779
-
780
- vm.expectRevert(
781
- abi.encodeWithSelector(JB721TiersHookStore.JB721TiersHookStore_UnrecognizedTier.selector, invalidTier)
782
- );
783
-
784
- vm.prank(mockTerminalAddress);
785
- hook.afterPayRecordedWith(
786
- JBAfterPayRecordedContext({
787
- payer: msg.sender,
788
- projectId: projectId,
789
- rulesetId: 0,
790
- amount: JBTokenAmount({
791
- token: JBConstants.NATIVE_TOKEN,
792
- value: tiers[0].price * 2 + tiers[1].price,
793
- decimals: 18,
794
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
795
- }),
796
- forwardedAmount: JBTokenAmount({
797
- token: JBConstants.NATIVE_TOKEN,
798
- value: 0,
799
- decimals: 18,
800
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
801
- }), // 0,
802
- // forwarded to the hook.
803
- weight: 10 ** 18,
804
- newlyIssuedTokenCount: 0,
805
- beneficiary: msg.sender,
806
- hookMetadata: new bytes(0),
807
- payerMetadata: hookMetadata
808
- })
809
- );
810
-
811
- // Check: has the total supply stayed the same?
812
- assertEq(totalSupplyBeforePay, hook.STORE().totalSupplyOf(address(hook)));
813
- }
814
-
815
- // If the amount is not enought to pay for all of the requested tiers, revert.
816
- function test_afterPayRecorded_revertIfAmountTooLow() public {
817
- // Mock the directory call.
818
- mockAndExpect(
819
- address(mockJBDirectory),
820
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
821
- abi.encode(true)
822
- );
823
-
824
- uint256 totalSupplyBeforePay = hook.STORE().totalSupplyOf(address(hook));
825
-
826
- bool allowOverspending;
827
- uint16[] memory tierIdsToMint = new uint16[](3);
828
- tierIdsToMint[0] = 1;
829
- tierIdsToMint[1] = 1;
830
- tierIdsToMint[2] = 2;
831
-
832
- // Build the metadata using the tiers to mint and the overspending flag.
833
- bytes[] memory data = new bytes[](1);
834
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
835
-
836
- // Pass the hook ID.
837
- bytes4[] memory ids = new bytes4[](1);
838
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
839
-
840
- // Generate the metadata.
841
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
842
-
843
- // Expect a revert for the amount being too low.
844
- vm.expectRevert(
845
- abi.encodeWithSelector(
846
- JB721TiersHookStore.JB721TiersHookStore_PriceExceedsAmount.selector, tiers[1].price, tiers[1].price - 1
847
- )
848
- );
849
-
850
- vm.prank(mockTerminalAddress);
851
- hook.afterPayRecordedWith(
852
- JBAfterPayRecordedContext({
853
- payer: msg.sender,
854
- projectId: projectId,
855
- rulesetId: 0,
856
- amount: JBTokenAmount({
857
- token: JBConstants.NATIVE_TOKEN,
858
- value: tiers[0].price * 2 + tiers[1].price - 1,
859
- decimals: 18,
860
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
861
- }),
862
- forwardedAmount: JBTokenAmount({
863
- token: JBConstants.NATIVE_TOKEN,
864
- value: 0,
865
- decimals: 18,
866
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
867
- }), // 0,
868
- // forwarded to the hook.
869
- weight: 10 ** 18,
870
- newlyIssuedTokenCount: 0,
871
- beneficiary: msg.sender,
872
- hookMetadata: new bytes(0),
873
- payerMetadata: hookMetadata
874
- })
875
- );
876
-
877
- // Check: has the total supply stayed the same?
878
- assertEq(totalSupplyBeforePay, hook.STORE().totalSupplyOf(address(hook)));
879
- }
880
-
881
- function test_afterPayRecorded_revertIfAllowanceRunsOutInSpecifiedTier() public {
882
- // Mock the directory call.
883
- mockAndExpect(
884
- address(mockJBDirectory),
885
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
886
- abi.encode(true)
887
- );
888
-
889
- uint256 supplyLeft = tiers[0].initialSupply;
890
-
891
- while (true) {
892
- uint256 totalSupplyBeforePay = hook.STORE().totalSupplyOf(address(hook));
893
-
894
- bool allowOverspending = true;
895
-
896
- uint16[] memory tierSelected = new uint16[](1);
897
- tierSelected[0] = 1;
898
-
899
- // Build the metadata using the tiers to mint and the overspending flag.
900
- bytes[] memory data = new bytes[](1);
901
- data[0] = abi.encode(allowOverspending, tierSelected);
902
-
903
- // Pass the hook ID.
904
- bytes4[] memory ids = new bytes4[](1);
905
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
906
-
907
- // Generate the metadata.
908
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
909
-
910
- // If there is no remaining supply, this should revert.
911
- if (supplyLeft == 0) {
912
- vm.expectRevert(
913
- abi.encodeWithSelector(
914
- JB721TiersHookStore.JB721TiersHookStore_InsufficientSupplyRemaining.selector, 1
915
- )
916
- );
917
- }
918
-
919
- // Execute the payment.
920
- vm.prank(mockTerminalAddress);
921
- hook.afterPayRecordedWith(
922
- JBAfterPayRecordedContext({
923
- payer: msg.sender,
924
- projectId: projectId,
925
- rulesetId: 0,
926
- amount: JBTokenAmount({
927
- token: JBConstants.NATIVE_TOKEN,
928
- value: tiers[0].price,
929
- decimals: 18,
930
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
931
- }),
932
- forwardedAmount: JBTokenAmount({
933
- token: JBConstants.NATIVE_TOKEN,
934
- value: 0,
935
- decimals: 18,
936
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
937
- }), // 0, forwarded to the hook.
938
- weight: 10 ** 18,
939
- newlyIssuedTokenCount: 0,
940
- beneficiary: msg.sender,
941
- hookMetadata: new bytes(0),
942
- payerMetadata: hookMetadata
943
- })
944
- );
945
- // If there's no supply left...
946
- if (supplyLeft == 0) {
947
- // Check: has the total supply stayed the same?
948
- assertEq(hook.STORE().totalSupplyOf(address(hook)), totalSupplyBeforePay);
949
- break;
950
- } else {
951
- // Otherwise, check that the total supply has increased by 1.
952
- assertEq(hook.STORE().totalSupplyOf(address(hook)), totalSupplyBeforePay + 1);
953
- }
954
- --supplyLeft;
955
- }
956
- }
957
-
958
- function test_afterPayRecorded_revertIfCallerIsNotATerminalOfProjectId(address terminal) public {
959
- vm.assume(terminal != mockTerminalAddress);
960
-
961
- // Mock the directory call.
962
- mockAndExpect(
963
- address(mockJBDirectory),
964
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, terminal),
965
- abi.encode(false)
966
- );
967
-
968
- // The caller is the `_expectedCaller`. However, the terminal in the calldata is not correct.
969
- vm.prank(terminal);
970
-
971
- // Expect a revert for the caller not being a terminal of the project.
972
- vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidPay.selector));
973
-
974
- hook.afterPayRecordedWith(
975
- JBAfterPayRecordedContext({
976
- payer: msg.sender,
977
- projectId: projectId,
978
- rulesetId: 0,
979
- amount: JBTokenAmount({
980
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
981
- }),
982
- forwardedAmount: JBTokenAmount({
983
- token: JBConstants.NATIVE_TOKEN,
984
- value: 0,
985
- decimals: 18,
986
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
987
- }), // 0,
988
- // forwarded to the hook.
989
- weight: 10 ** 18,
990
- newlyIssuedTokenCount: 0,
991
- beneficiary: msg.sender,
992
- hookMetadata: new bytes(0),
993
- payerMetadata: new bytes(0)
994
- })
995
- );
996
- }
997
-
998
- function test_afterPayRecorded_silentlyReturnsOnCurrencyMismatchWithoutPriceFeed(address token) public {
999
- vm.assume(token != JBConstants.NATIVE_TOKEN);
1000
-
1001
- // Deploy a hook with PRICES = address(0) to test the no-prices path.
1002
- JB721TiersHook noPricesOrigin = new JB721TiersHook(
1003
- IJBDirectory(mockJBDirectory),
1004
- IJBPermissions(mockJBPermissions),
1005
- IJBPrices(address(0)),
1006
- IJBRulesets(mockJBRulesets),
1007
- IJB721TiersHookStore(store),
1008
- IJBSplits(mockJBSplits),
1009
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
1010
- trustedForwarder
1011
- );
1012
- address noPricesProxy = makeAddr("noPricesProxy2");
1013
- vm.etch(noPricesProxy, address(noPricesOrigin).code);
1014
- JB721TiersHook noPricesHook = JB721TiersHook(noPricesProxy);
1015
-
1016
- (JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 10);
1017
- noPricesHook.initialize(
1018
- projectId,
1019
- name,
1020
- symbol,
1021
- baseUri,
1022
- IJB721TokenUriResolver(mockTokenUriResolver),
1023
- contractUri,
1024
- JB721InitTiersConfig({
1025
- tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
1026
- }),
1027
- JB721TiersHookFlags({
1028
- preventOverspending: false,
1029
- issueTokensForSplits: false,
1030
- noNewTiersWithReserves: false,
1031
- noNewTiersWithVotes: false,
1032
- noNewTiersWithOwnerMinting: false
1033
- })
1034
- );
1035
-
1036
- // Mock the directory call.
1037
- mockAndExpect(
1038
- address(mockJBDirectory),
1039
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1040
- abi.encode(true)
1041
- );
1042
-
1043
- // The payment's currency (18) doesn't match the hook's pricing currency (native token).
1044
- // With PRICES = address(0), normalizePaymentValue returns (0, false) → silently returns.
1045
- vm.prank(mockTerminalAddress);
1046
- noPricesHook.afterPayRecordedWith(
1047
- JBAfterPayRecordedContext({
1048
- payer: msg.sender,
1049
- projectId: projectId,
1050
- rulesetId: 0,
1051
- amount: JBTokenAmount({
1052
- token: token, decimals: 0, currency: 18, value: uint32(uint160(JBConstants.NATIVE_TOKEN))
1053
- }),
1054
- forwardedAmount: JBTokenAmount({
1055
- token: JBConstants.NATIVE_TOKEN,
1056
- decimals: 0,
1057
- currency: 18,
1058
- value: uint32(uint160(JBConstants.NATIVE_TOKEN))
1059
- }),
1060
- // forwarded to the hook.
1061
- weight: 10 ** 18,
1062
- newlyIssuedTokenCount: 0,
1063
- beneficiary: msg.sender,
1064
- hookMetadata: new bytes(0),
1065
- payerMetadata: new bytes(0)
1066
- })
1067
- );
1068
-
1069
- // Verify no credits were added (the function returned early).
1070
- assertEq(noPricesHook.payCreditsOf(msg.sender), 0);
1071
- }
1072
-
1073
- function test_afterPayRecorded_mintWithExistingCreditsWhenMoreExistingCreditsThanNewCredits() public {
1074
- uint256 leftover = tiers[0].price + 1; // + 1 to avoid rounding error.
1075
- uint256 amount = tiers[0].price * 2 + tiers[1].price + leftover / 2;
1076
-
1077
- // Mock the directory call.
1078
- mockAndExpect(
1079
- address(mockJBDirectory),
1080
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1081
- abi.encode(true)
1082
- );
1083
-
1084
- bool allowOverspending = true;
1085
- uint16[] memory tierIdsToMint = new uint16[](3);
1086
- tierIdsToMint[0] = 1;
1087
- tierIdsToMint[1] = 1;
1088
- tierIdsToMint[2] = 2;
1089
-
1090
- // Build the metadata using the tiers to mint and the overspending flag.
1091
- bytes[] memory data = new bytes[](1);
1092
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
1093
-
1094
- // Pass the hook ID.
1095
- bytes4[] memory ids = new bytes4[](1);
1096
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
1097
-
1098
- // Generate the metadata.
1099
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
1100
-
1101
- uint256 credits = hook.payCreditsOf(beneficiary);
1102
- leftover = leftover / 2 + credits; // Leftover amount.
1103
-
1104
- vm.expectEmit(true, true, true, true, address(hook));
1105
- emit AddPayCredits(leftover - credits, leftover, beneficiary, mockTerminalAddress);
1106
-
1107
- // The first call will mint the 3 tiers requested and accumulate half of the first price as pay credits.
1108
- vm.prank(mockTerminalAddress);
1109
- hook.afterPayRecordedWith(
1110
- JBAfterPayRecordedContext({
1111
- payer: beneficiary,
1112
- projectId: projectId,
1113
- rulesetId: 0,
1114
- amount: JBTokenAmount({
1115
- token: JBConstants.NATIVE_TOKEN,
1116
- value: amount,
1117
- decimals: 18,
1118
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1119
- }),
1120
- forwardedAmount: JBTokenAmount({
1121
- token: JBConstants.NATIVE_TOKEN,
1122
- value: 0,
1123
- decimals: 18,
1124
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1125
- }), // 0,
1126
- // forwarded to the hook.
1127
- weight: 10 ** 18,
1128
- newlyIssuedTokenCount: 0,
1129
- beneficiary: beneficiary,
1130
- hookMetadata: new bytes(0),
1131
- payerMetadata: hookMetadata
1132
- })
1133
- );
1134
-
1135
- uint256 totalSupplyBefore = hook.STORE().totalSupplyOf(address(hook));
1136
- {
1137
- // We now attempt to mint an additional NFT from tier 1 by using the pay credits we collected from the last
1138
- // payment.
1139
- uint16[] memory moreTierIdsToMint = new uint16[](1);
1140
- moreTierIdsToMint[0] = 1;
1141
-
1142
- data[0] = abi.encode(allowOverspending, moreTierIdsToMint);
1143
-
1144
- // Generate the metadata.
1145
- hookMetadata = metadataHelper.createMetadata(ids, data);
1146
- }
1147
-
1148
- // Fetch the existing pay credits.
1149
- credits = hook.payCreditsOf(beneficiary);
1150
-
1151
- // Use existing credits to mint.
1152
- leftover = tiers[0].price - 1 - credits;
1153
- vm.expectEmit(true, true, true, true, address(hook));
1154
- emit UsePayCredits(credits - leftover, leftover, beneficiary, mockTerminalAddress);
1155
-
1156
- // Mint with leftover pay credits.
1157
- vm.prank(mockTerminalAddress);
1158
- hook.afterPayRecordedWith(
1159
- JBAfterPayRecordedContext({
1160
- payer: beneficiary,
1161
- projectId: projectId,
1162
- rulesetId: 0,
1163
- amount: JBTokenAmount({
1164
- token: JBConstants.NATIVE_TOKEN,
1165
- value: tiers[0].price - 1,
1166
- decimals: 18,
1167
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1168
- }),
1169
- forwardedAmount: JBTokenAmount({
1170
- token: JBConstants.NATIVE_TOKEN,
1171
- value: 0,
1172
- decimals: 18,
1173
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1174
- }), // 0,
1175
- // forwarded to the hook.
1176
- weight: 10 ** 18,
1177
- newlyIssuedTokenCount: 0,
1178
- beneficiary: beneficiary,
1179
- hookMetadata: new bytes(0),
1180
- payerMetadata: hookMetadata
1181
- })
1182
- );
1183
-
1184
- // Check: has the total supply increased by 1?
1185
- assertEq(totalSupplyBefore + 1, hook.STORE().totalSupplyOf(address(hook)));
1186
- }
1187
-
1188
- function test_afterPayRecorded_revertIfUnexpectedLeftover() public {
1189
- uint256 leftover = tiers[1].price - 1;
1190
- uint256 amount = tiers[0].price + leftover;
1191
-
1192
- // Mock the directory call.
1193
- mockAndExpect(
1194
- address(mockJBDirectory),
1195
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1196
- abi.encode(true)
1197
- );
1198
- bool allowOverspending;
1199
- uint16[] memory tierIdsToMint = new uint16[](0);
1200
-
1201
- // Build the metadata using the tiers to mint and the overspending flag.
1202
- bytes[] memory data = new bytes[](1);
1203
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
1204
-
1205
- // Pass the hook ID.
1206
- bytes4[] memory ids = new bytes4[](1);
1207
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
1208
-
1209
- // Generate the metadata.
1210
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
1211
- vm.prank(mockTerminalAddress);
1212
- vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_Overspending.selector, amount));
1213
- hook.afterPayRecordedWith(
1214
- JBAfterPayRecordedContext({
1215
- payer: msg.sender,
1216
- projectId: projectId,
1217
- rulesetId: 0,
1218
- amount: JBTokenAmount({
1219
- token: JBConstants.NATIVE_TOKEN,
1220
- value: amount,
1221
- decimals: 18,
1222
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1223
- }),
1224
- forwardedAmount: JBTokenAmount({
1225
- token: JBConstants.NATIVE_TOKEN,
1226
- value: 0,
1227
- decimals: 18,
1228
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1229
- }), // 0,
1230
- // forwarded to the hook.
1231
- weight: 10 ** 18,
1232
- newlyIssuedTokenCount: 0,
1233
- beneficiary: beneficiary,
1234
- hookMetadata: new bytes(0),
1235
- payerMetadata: hookMetadata
1236
- })
1237
- );
1238
- }
1239
-
1240
- function test_afterPayRecorded_revertIfUnexpectedLeftoverAndOverspendingPrevented(bool prevent) public {
1241
- uint256 leftover = tiers[1].price - 1;
1242
- uint256 amount = tiers[0].price + leftover;
1243
-
1244
- // Mock the directory call.
1245
- mockAndExpect(
1246
- address(mockJBDirectory),
1247
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1248
- abi.encode(true)
1249
- );
1250
-
1251
- // Get the current flags.
1252
- JB721TiersHookFlags memory flags = hook.STORE().flagsOf(address(hook));
1253
-
1254
- // Set the prevent flag to the given value.
1255
- flags.preventOverspending = prevent;
1256
-
1257
- // Mock the call to return the new flags.
1258
- mockAndExpect(
1259
- address(hook.STORE()),
1260
- abi.encodeWithSelector(IJB721TiersHookStore.flagsOf.selector, address(hook)),
1261
- abi.encode(flags)
1262
- );
1263
-
1264
- bool allowOverspending = true;
1265
- uint16[] memory tierIdsToMint = new uint16[](0);
1266
-
1267
- bytes memory metadata =
1268
- abi.encode(bytes32(0), bytes32(0), type(IJB721TiersHook).interfaceId, allowOverspending, tierIdsToMint);
1269
-
1270
- // If prevent is enabled the call should revert. Otherwise, we should receive pay credits.
1271
- if (prevent) {
1272
- vm.expectRevert(abi.encodeWithSelector(JB721TiersHook.JB721TiersHook_Overspending.selector, amount));
1273
- } else {
1274
- uint256 payCredits = hook.payCreditsOf(beneficiary);
1275
- uint256 stashedPayCredits = payCredits;
1276
- // Calculating new pay credit balance (since leftover is non-zero).
1277
- uint256 newPayCredits = tiers[0].price + leftover + stashedPayCredits;
1278
- vm.expectEmit(true, true, true, true, address(hook));
1279
- emit AddPayCredits(newPayCredits - payCredits, newPayCredits, beneficiary, mockTerminalAddress);
1280
- }
1281
- vm.prank(mockTerminalAddress);
1282
- hook.afterPayRecordedWith(
1283
- JBAfterPayRecordedContext({
1284
- payer: msg.sender,
1285
- projectId: projectId,
1286
- rulesetId: 0,
1287
- amount: JBTokenAmount({
1288
- token: JBConstants.NATIVE_TOKEN,
1289
- value: amount,
1290
- decimals: 18,
1291
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1292
- }),
1293
- forwardedAmount: JBTokenAmount({
1294
- token: JBConstants.NATIVE_TOKEN,
1295
- value: 0,
1296
- decimals: 18,
1297
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1298
- }), // 0,
1299
- // forwarded to the hook.
1300
- weight: 10 ** 18,
1301
- newlyIssuedTokenCount: 0,
1302
- beneficiary: beneficiary,
1303
- hookMetadata: new bytes(0),
1304
- payerMetadata: metadata
1305
- })
1306
- );
1307
- }
1308
-
1309
- // If transfers are paused, transfers which do not involve the zero address are reverted,
1310
- // as long as the `transfersPausable` flag must be true.
1311
- // Transfers involving the zero address (minting and burning) are not affected.
1312
- function test_transferFrom_revertTransferIfPausedInRuleset() public {
1313
- defaultTierConfig.flags.transfersPausable = true;
1314
- JB721TiersHook hook = _initHookDefaultTiers(10);
1315
-
1316
- // Mock the directory call.
1317
- mockAndExpect(
1318
- address(mockJBDirectory),
1319
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1320
- abi.encode(true)
1321
- );
1322
-
1323
- mockAndExpect(
1324
- mockJBRulesets,
1325
- abi.encodeCall(IJBRulesets.currentOf, projectId),
1326
- abi.encode(
1327
- JBRuleset({
1328
- cycleNumber: 1,
1329
- id: uint48(block.timestamp),
1330
- basedOnId: 0,
1331
- start: uint48(block.timestamp),
1332
- duration: 600,
1333
- weight: 10e18,
1334
- weightCutPercent: 0,
1335
- approvalHook: IJBRulesetApprovalHook(address(0)),
1336
- metadata: JBRulesetMetadataResolver.packRulesetMetadata(
1337
- JBRulesetMetadata({
1338
- reservedPercent: 5000, //50%
1339
- cashOutTaxRate: 5000, //50%
1340
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
1341
- pausePay: false,
1342
- pauseCreditTransfers: false,
1343
- allowOwnerMinting: true,
1344
- allowSetCustomToken: false,
1345
- allowTerminalMigration: false,
1346
- allowSetTerminals: false,
1347
- allowSetController: false,
1348
- allowAddAccountingContext: false,
1349
- allowAddPriceFeed: false,
1350
- ownerMustSendPayouts: false,
1351
- holdFees: false,
1352
- useTotalSurplusForCashOuts: false,
1353
- useDataHookForPay: true,
1354
- useDataHookForCashOut: true,
1355
- dataHook: address(hook),
1356
- metadata: 1
1357
- })
1358
- )
1359
- })
1360
- )
1361
- );
1362
-
1363
- bool allowOverspending;
1364
- uint16[] memory tierIdsToMint = new uint16[](3);
1365
- tierIdsToMint[0] = 1;
1366
- tierIdsToMint[1] = 1;
1367
- tierIdsToMint[2] = 2;
1368
-
1369
- // Build the metadata using the tiers to mint and the overspending flag.
1370
- bytes[] memory data = new bytes[](1);
1371
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
1372
-
1373
- // Pass the hook ID.
1374
- bytes4[] memory ids = new bytes4[](1);
1375
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
1376
-
1377
- // Generate the metadata.
1378
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
1379
-
1380
- vm.prank(mockTerminalAddress);
1381
- hook.afterPayRecordedWith(
1382
- JBAfterPayRecordedContext({
1383
- payer: msg.sender,
1384
- projectId: projectId,
1385
- rulesetId: 0,
1386
- amount: JBTokenAmount({
1387
- token: JBConstants.NATIVE_TOKEN,
1388
- value: tiers[0].price * 2 + tiers[1].price,
1389
- decimals: 18,
1390
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1391
- }),
1392
- forwardedAmount: JBTokenAmount({
1393
- token: JBConstants.NATIVE_TOKEN,
1394
- value: 0,
1395
- decimals: 18,
1396
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1397
- }), // 0,
1398
- // forwarded to the hook.
1399
- weight: 10 ** 18,
1400
- newlyIssuedTokenCount: 0,
1401
- beneficiary: msg.sender,
1402
- hookMetadata: new bytes(0),
1403
- payerMetadata: hookMetadata
1404
- })
1405
- );
1406
-
1407
- uint256 tokenId = _generateTokenId(1, 1);
1408
-
1409
- // Expect a revert on account of transfers being paused.
1410
- vm.expectRevert(JB721TiersHook.JB721TiersHook_TierTransfersPaused.selector);
1411
-
1412
- vm.prank(msg.sender);
1413
- IERC721(hook).transferFrom(msg.sender, beneficiary, tokenId);
1414
- }
1415
-
1416
- // If the ruleset metadata has `pauseTransfers` enabled,
1417
- // BUT the tier being transferred has `transfersPausable` disabled,
1418
- // transfer are not paused (this bypasses the call to `JBRulesets`).
1419
- function test_transferFrom_pauseFlagOverridesRuleset() public {
1420
- // Mock the directory call.
1421
- mockAndExpect(
1422
- address(mockJBDirectory),
1423
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1424
- abi.encode(true)
1425
- );
1426
-
1427
- JB721TiersHook hook = _initHookDefaultTiers(10);
1428
-
1429
- bool allowOverspending;
1430
- uint16[] memory tierIdsToMint = new uint16[](3);
1431
- tierIdsToMint[0] = 1;
1432
- tierIdsToMint[1] = 1;
1433
- tierIdsToMint[2] = 2;
1434
-
1435
- // Build the metadata using the tiers to mint and the overspending flag.
1436
- bytes[] memory data = new bytes[](1);
1437
- data[0] = abi.encode(allowOverspending, tierIdsToMint);
1438
-
1439
- // Pass the hook ID.
1440
- bytes4[] memory ids = new bytes4[](1);
1441
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
1442
-
1443
- // Generate the metadata.
1444
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
1445
-
1446
- vm.prank(mockTerminalAddress);
1447
- hook.afterPayRecordedWith(
1448
- JBAfterPayRecordedContext({
1449
- payer: msg.sender,
1450
- projectId: projectId,
1451
- rulesetId: 0,
1452
- amount: JBTokenAmount({
1453
- token: JBConstants.NATIVE_TOKEN,
1454
- value: tiers[0].price * 2 + tiers[1].price,
1455
- decimals: 18,
1456
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1457
- }),
1458
- forwardedAmount: JBTokenAmount({
1459
- token: JBConstants.NATIVE_TOKEN,
1460
- value: 0,
1461
- decimals: 18,
1462
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1463
- }), // 0,
1464
- // forwarded to the hook.
1465
- weight: 10 ** 18,
1466
- newlyIssuedTokenCount: 0,
1467
- beneficiary: msg.sender,
1468
- hookMetadata: new bytes(0),
1469
- payerMetadata: hookMetadata
1470
- })
1471
- );
1472
-
1473
- uint256 tokenId = _generateTokenId(1, 1);
1474
- vm.prank(msg.sender);
1475
- IERC721(hook).transferFrom(msg.sender, beneficiary, tokenId);
1476
- // Check: was the NFT transferred to the beneficiary?
1477
- assertEq(IERC721(hook).ownerOf(tokenId), beneficiary);
1478
- }
1479
-
1480
- // Cash out an NFT, even if transfers are paused in the ruleset metadata. This should bypass the call to
1481
- // `JBRulesets`.
1482
- function test_afterCashOutRecordedWith_cashOutEvenIfTransfersPausedInRuleset() public {
1483
- address holder = address(bytes20(keccak256("holder")));
1484
-
1485
- JB721TiersHook hook = _initHookDefaultTiers(10);
1486
-
1487
- // Mock the directory call.
1488
- mockAndExpect(
1489
- address(mockJBDirectory),
1490
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1491
- abi.encode(true)
1492
- );
1493
-
1494
- // Build the metadata which will be used to mint.
1495
- bytes memory hookMetadata;
1496
- bytes[] memory data = new bytes[](1);
1497
- bytes4[] memory ids = new bytes4[](1);
1498
-
1499
- {
1500
- // Craft the metadata: mint the specified tier.
1501
- uint16[] memory rawMetadata = new uint16[](1);
1502
- rawMetadata[0] = uint16(1); // 1 indexed
1503
-
1504
- // Build the metadata using the tiers to mint and the overspending flag.
1505
- data[0] = abi.encode(true, rawMetadata);
1506
-
1507
- // Pass the hook ID.
1508
- ids[0] = metadataHelper.getId("pay", address(hook));
1509
-
1510
- // Generate the metadata.
1511
- hookMetadata = metadataHelper.createMetadata(ids, data);
1512
- }
1513
-
1514
- // Mint the NFTs. Otherwise, the voting balance is not incremented which leads to an underflow upon cash outs.
1515
- vm.prank(mockTerminalAddress);
1516
- hook.afterPayRecordedWith(
1517
- JBAfterPayRecordedContext({
1518
- payer: holder,
1519
- projectId: projectId,
1520
- rulesetId: 0,
1521
- amount: JBTokenAmount({
1522
- token: JBConstants.NATIVE_TOKEN,
1523
- value: tiers[0].price,
1524
- decimals: 18,
1525
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1526
- }),
1527
- forwardedAmount: JBTokenAmount({
1528
- token: JBConstants.NATIVE_TOKEN,
1529
- value: 0,
1530
- decimals: 18,
1531
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1532
- }), // 0,
1533
- // forwarded to the hook.
1534
- weight: 10 ** 18,
1535
- newlyIssuedTokenCount: 0,
1536
- beneficiary: holder,
1537
- hookMetadata: new bytes(0),
1538
- payerMetadata: hookMetadata
1539
- })
1540
- );
1541
-
1542
- uint256[] memory tokenToCashOut = new uint256[](1);
1543
- tokenToCashOut[0] = _generateTokenId(1, 1);
1544
-
1545
- // Build the metadata with the tiers to cash out.
1546
- data[0] = abi.encode(tokenToCashOut);
1547
-
1548
- // Pass the hook ID.
1549
- ids[0] = metadataHelper.getId("pay", address(hookOrigin));
1550
-
1551
- // Generate the metadata.
1552
- hookMetadata = metadataHelper.createMetadata(ids, data);
1553
-
1554
- vm.prank(mockTerminalAddress);
1555
- hook.afterCashOutRecordedWith(
1556
- JBAfterCashOutRecordedContext({
1557
- holder: holder,
1558
- projectId: projectId,
1559
- rulesetId: 1,
1560
- cashOutCount: 0,
1561
- reclaimedAmount: JBTokenAmount({
1562
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1563
- }),
1564
- forwardedAmount: JBTokenAmount({
1565
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1566
- }), // 0, forwarded to the hook.
1567
- cashOutTaxRate: 5000,
1568
- beneficiary: payable(holder),
1569
- hookMetadata: bytes(""),
1570
- cashOutMetadata: hookMetadata
1571
- })
1572
- );
1573
-
1574
- // Check: has the holder's balance returned to 0?
1575
- assertEq(hook.balanceOf(holder), 0);
1576
- }
1577
-
1578
- function test_afterPayRecorded_revertOnCreditOverflow_samePayerBeneficiary() public {
1579
- // Mock the directory call.
1580
- mockAndExpect(
1581
- address(mockJBDirectory),
1582
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1583
- abi.encode(true)
1584
- );
1585
-
1586
- // Set the beneficiary's pay credits to max uint256.
1587
- stdstore.target(address(hook)).sig("payCreditsOf(address)").with_key(beneficiary)
1588
- .checked_write(type(uint256).max);
1589
-
1590
- // Pay 1 wei where payer == beneficiary. No metadata → no NFT mints.
1591
- // `leftoverAmount += payCredits` overflows: 1 + type(uint256).max.
1592
- vm.expectRevert(stdError.arithmeticError);
1593
- vm.prank(mockTerminalAddress);
1594
- hook.afterPayRecordedWith(
1595
- JBAfterPayRecordedContext({
1596
- payer: beneficiary,
1597
- projectId: projectId,
1598
- rulesetId: 0,
1599
- amount: JBTokenAmount({
1600
- token: JBConstants.NATIVE_TOKEN,
1601
- value: 1,
1602
- decimals: 18,
1603
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1604
- }),
1605
- forwardedAmount: JBTokenAmount({
1606
- token: JBConstants.NATIVE_TOKEN,
1607
- value: 0,
1608
- decimals: 18,
1609
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1610
- }),
1611
- weight: 10 ** 18,
1612
- newlyIssuedTokenCount: 0,
1613
- beneficiary: beneficiary,
1614
- hookMetadata: new bytes(0),
1615
- payerMetadata: new bytes(0)
1616
- })
1617
- );
1618
- }
1619
-
1620
- function test_afterPayRecorded_revertOnCreditOverflow_differentPayerBeneficiary() public {
1621
- // Mock the directory call.
1622
- mockAndExpect(
1623
- address(mockJBDirectory),
1624
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
1625
- abi.encode(true)
1626
- );
1627
-
1628
- // Set the beneficiary's pay credits to max uint256.
1629
- stdstore.target(address(hook)).sig("payCreditsOf(address)").with_key(beneficiary)
1630
- .checked_write(type(uint256).max);
1631
-
1632
- // Pay 1 wei where payer != beneficiary. No metadata → no NFT mints, overspending allowed.
1633
- // leftoverAmount=1, unusedPayCredits=type(uint256).max → overflow in `leftoverAmount + unusedPayCredits`.
1634
- vm.expectRevert(stdError.arithmeticError);
1635
- vm.prank(mockTerminalAddress);
1636
- hook.afterPayRecordedWith(
1637
- JBAfterPayRecordedContext({
1638
- payer: address(0xdead),
1639
- projectId: projectId,
1640
- rulesetId: 0,
1641
- amount: JBTokenAmount({
1642
- token: JBConstants.NATIVE_TOKEN,
1643
- value: 1,
1644
- decimals: 18,
1645
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1646
- }),
1647
- forwardedAmount: JBTokenAmount({
1648
- token: JBConstants.NATIVE_TOKEN,
1649
- value: 0,
1650
- decimals: 18,
1651
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
1652
- }),
1653
- weight: 10 ** 18,
1654
- newlyIssuedTokenCount: 0,
1655
- beneficiary: beneficiary,
1656
- hookMetadata: new bytes(0),
1657
- payerMetadata: new bytes(0)
1658
- })
1659
- );
1660
- }
1661
- }