@bananapus/721-hook-v6 0.0.42 → 0.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +20 -9
  4. package/script/Deploy.s.sol +2 -2
  5. package/src/JB721Checkpoints.sol +61 -19
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +66 -53
  8. package/src/JB721TiersHookDeployer.sol +8 -5
  9. package/src/JB721TiersHookProjectDeployer.sol +87 -46
  10. package/src/JB721TiersHookStore.sol +137 -107
  11. package/src/abstract/JB721Hook.sol +8 -6
  12. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  13. package/src/interfaces/IJB721CheckpointsDeployer.sol +7 -3
  14. package/src/interfaces/IJB721TiersHook.sol +3 -3
  15. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +4 -2
  16. package/src/interfaces/IJB721TiersHookStore.sol +11 -11
  17. package/src/libraries/JB721TiersHookLib.sol +1 -1
  18. package/src/structs/JB721TiersHookFlags.sol +1 -1
  19. package/src/structs/JBPayDataHookRulesetMetadata.sol +1 -1
  20. package/test/utils/AccessJBLib.sol +49 -0
  21. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  22. package/test/utils/TestBaseWorkflow.sol +213 -0
  23. package/test/utils/UnitTestSetup.sol +805 -0
  24. package/.gas-snapshot +0 -152
  25. package/ADMINISTRATION.md +0 -87
  26. package/ARCHITECTURE.md +0 -98
  27. package/AUDIT_INSTRUCTIONS.md +0 -77
  28. package/RISKS.md +0 -118
  29. package/SKILLS.md +0 -43
  30. package/STYLE_GUIDE.md +0 -610
  31. package/USER_JOURNEYS.md +0 -121
  32. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  33. package/slither-ci.config.json +0 -10
  34. package/test/721HookAttacks.t.sol +0 -408
  35. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  36. package/test/Fork.t.sol +0 -2346
  37. package/test/TestAuditGaps.sol +0 -1075
  38. package/test/TestCheckpoints.t.sol +0 -341
  39. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  40. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  41. package/test/audit/AuditRegressions.t.sol +0 -83
  42. package/test/audit/CodexNemesisReserveSellout.t.sol +0 -66
  43. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  44. package/test/audit/FreshAudit.t.sol +0 -197
  45. package/test/audit/FutureTierPoC.t.sol +0 -39
  46. package/test/audit/FutureTierRemoval.t.sol +0 -47
  47. package/test/audit/Pass12L18.t.sol +0 -80
  48. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  49. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  50. package/test/audit/RepoFindings.t.sol +0 -195
  51. package/test/audit/ReserveActivation.t.sol +0 -87
  52. package/test/audit/ReserveSlotProtection.t.sol +0 -273
  53. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  54. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  55. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  56. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  57. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  58. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  59. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  60. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  61. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  62. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  63. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  64. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  65. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  66. package/test/regression/CacheTierLookup.t.sol +0 -190
  67. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  68. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  69. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  70. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  71. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  72. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  73. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  74. package/test/unit/JBBitmap.t.sol +0 -170
  75. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  76. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  77. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  78. package/test/unit/deployer_Unit.t.sol +0 -114
  79. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  80. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  81. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  82. package/test/unit/pay_Unit.t.sol +0 -1661
  83. package/test/unit/redeem_Unit.t.sol +0 -473
  84. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  85. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  86. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
@@ -1,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
- }