@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,473 +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_cashOut_Unit is UnitTestSetup {
8
- using stdStorage for StdStorage;
9
-
10
- function test_beforeCashOutContext_returnsCorrectAmount() public {
11
- uint256 weight;
12
- uint256 totalWeight;
13
- ForTest_JB721TiersHook hook = _initializeForTestHook(10);
14
-
15
- // Set up 10 tiers, with half of the supply minted for each one.
16
- for (uint256 i = 1; i <= 10; i++) {
17
- hook.test_store()
18
- .ForTest_setTier(
19
- address(hook),
20
- i,
21
- JBStored721Tier({
22
- // forge-lint: disable-next-line(unsafe-typecast)
23
- price: uint104(i * 10),
24
- // forge-lint: disable-next-line(unsafe-typecast)
25
- remainingSupply: uint32(10 * i - 5 * i),
26
- // forge-lint: disable-next-line(unsafe-typecast)
27
- initialSupply: uint32(10 * i),
28
- reserveFrequency: uint16(0),
29
- category: uint24(100),
30
- discountPercent: uint8(0),
31
- packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false, false),
32
- splitPercent: 0
33
- })
34
- );
35
- totalWeight += (10 * i - 5 * i) * i * 10;
36
- }
37
-
38
- // Cash out as if the beneficiary has 1 NFT from each of the first five tiers.
39
- uint256[] memory tokenList = new uint256[](5);
40
- for (uint256 i; i < 5; i++) {
41
- uint256 tokenId = _generateTokenId(i + 1, 1);
42
- hook.ForTest_setOwnerOf(tokenId, beneficiary);
43
- tokenList[i] = tokenId;
44
- weight += (i + 1) * 10;
45
- }
46
-
47
- // Build the metadata with the tiers to cash out.
48
- bytes[] memory data = new bytes[](1);
49
- data[0] = abi.encode(tokenList);
50
-
51
- // Pass the hook ID.
52
- bytes4[] memory ids = new bytes4[](1);
53
- ids[0] = metadataHelper.getId("cashOut", address(hookOrigin));
54
-
55
- // Generate the metadata.
56
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
57
- (uint256 cashOutTaxRate,,,, JBCashOutHookSpecification[] memory returnedHook) = hook.beforeCashOutRecordedWith(
58
- JBBeforeCashOutRecordedContext({
59
- terminal: address(0),
60
- holder: beneficiary,
61
- projectId: projectId,
62
- rulesetId: 0,
63
- cashOutCount: 0,
64
- totalSupply: 0,
65
- surplus: JBTokenAmount({
66
- token: address(0), value: SURPLUS, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
67
- }),
68
- useTotalSurplus: true,
69
- cashOutTaxRate: CASH_OUT_TAX_RATE,
70
- beneficiaryIsFeeless: false,
71
- metadata: hookMetadata
72
- })
73
- );
74
-
75
- // Check: does the reclaim amount match the expected value?
76
- assertEq(cashOutTaxRate, CASH_OUT_TAX_RATE);
77
- // Check: does the returned hook address match the expected value?
78
- assertEq(address(returnedHook[0].hook), address(hook));
79
- }
80
-
81
- function test_beforeCashOutContext_returnsZeroAmountIfReserveFrequencyIsZero() public {
82
- uint256 surplus = 10e18;
83
- uint256 cashOutTaxRate = 0;
84
- uint256 weight;
85
- uint256 totalWeight;
86
- JBCashOutHookSpecification[] memory returnedHook;
87
-
88
- ForTest_JB721TiersHook hook = _initializeForTestHook(10);
89
-
90
- // Set up 10 tiers, with half of the supply minted for each one.
91
- for (uint256 i = 1; i <= 10; i++) {
92
- hook.test_store()
93
- .ForTest_setTier(
94
- address(hook),
95
- i,
96
- JBStored721Tier({
97
- // forge-lint: disable-next-line(unsafe-typecast)
98
- price: uint104(i * 10),
99
- // forge-lint: disable-next-line(unsafe-typecast)
100
- remainingSupply: uint32(10 * i - 5 * i),
101
- // forge-lint: disable-next-line(unsafe-typecast)
102
- initialSupply: uint32(10 * i),
103
- reserveFrequency: uint16(0),
104
- category: uint24(100),
105
- discountPercent: uint8(0),
106
- packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false, false),
107
- splitPercent: 0
108
- })
109
- );
110
- totalWeight += (10 * i - 5 * i) * i * 10;
111
- }
112
-
113
- // Cash out as if the beneficiary has 1 NFT from each of the first five tiers.
114
- uint256[] memory tokenList = new uint256[](5);
115
- for (uint256 i; i < 5; i++) {
116
- hook.ForTest_setOwnerOf(i + 1, beneficiary);
117
- tokenList[i] = i + 1;
118
- weight += (i + 1) * (i + 1) * 10;
119
- }
120
-
121
- (cashOutTaxRate,,,, returnedHook) = hook.beforeCashOutRecordedWith(
122
- JBBeforeCashOutRecordedContext({
123
- terminal: address(0),
124
- holder: beneficiary,
125
- projectId: projectId,
126
- rulesetId: 0,
127
- cashOutCount: 0,
128
- totalSupply: 0,
129
- surplus: JBTokenAmount({
130
- token: address(0), value: surplus, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
131
- }),
132
- useTotalSurplus: true,
133
- cashOutTaxRate: cashOutTaxRate,
134
- beneficiaryIsFeeless: false,
135
- metadata: abi.encode(bytes32(0), type(IJB721TiersHook).interfaceId, tokenList)
136
- })
137
- );
138
-
139
- // Check: is the cash out tax rate zero?
140
- assertEq(cashOutTaxRate, 0);
141
- // Check: does the returned hook address match the expected value?
142
- assertEq(address(returnedHook[0].hook), address(hook));
143
- }
144
-
145
- function test_beforeCashOutContext_returnsPartOfOverflowOwnedIfCashOutTaxRateIsMaximum() public {
146
- uint256 weight;
147
- uint256 totalWeight;
148
-
149
- ForTest_JB721TiersHook hook = _initializeForTestHook(10);
150
-
151
- // Set up 10 tiers, with half of the supply minted for each one.
152
- for (uint256 i = 1; i <= 10; i++) {
153
- hook.test_store()
154
- .ForTest_setTier(
155
- address(hook),
156
- i,
157
- JBStored721Tier({
158
- // forge-lint: disable-next-line(unsafe-typecast)
159
- price: uint104(i * 10),
160
- // forge-lint: disable-next-line(unsafe-typecast)
161
- remainingSupply: uint32(10 * i - 5 * i),
162
- // forge-lint: disable-next-line(unsafe-typecast)
163
- initialSupply: uint32(10 * i),
164
- reserveFrequency: uint16(0),
165
- category: uint24(100),
166
- discountPercent: uint8(0),
167
- packedBools: hook.test_store().ForTest_packBools(false, false, false, false, false, false),
168
- splitPercent: 0
169
- })
170
- );
171
- totalWeight += (10 * i - 5 * i) * i * 10;
172
- }
173
-
174
- // Cash out as if the beneficiary has 1 NFT from each of the first five tiers.
175
- uint256[] memory tokenList = new uint256[](5);
176
- for (uint256 i; i < 5; i++) {
177
- hook.ForTest_setOwnerOf(_generateTokenId(i + 1, 1), beneficiary);
178
- tokenList[i] = _generateTokenId(i + 1, 1);
179
- weight += (i + 1) * 10;
180
- }
181
-
182
- // Build the metadata with the tiers to cash out.
183
- bytes[] memory data = new bytes[](1);
184
- data[0] = abi.encode(tokenList);
185
-
186
- // Pass the hook ID.
187
- bytes4[] memory ids = new bytes4[](1);
188
- ids[0] = metadataHelper.getId("cashOut", address(hookOrigin));
189
-
190
- // Generate the metadata.
191
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
192
-
193
- JBBeforeCashOutRecordedContext memory beforeCashOutContext = JBBeforeCashOutRecordedContext({
194
- terminal: address(0),
195
- holder: beneficiary,
196
- projectId: projectId,
197
- rulesetId: 0,
198
- cashOutCount: 0,
199
- totalSupply: 0,
200
- surplus: JBTokenAmount({
201
- token: address(0), value: SURPLUS, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
202
- }),
203
- useTotalSurplus: true,
204
- cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE,
205
- beneficiaryIsFeeless: false,
206
- metadata: hookMetadata
207
- });
208
-
209
- (uint256 cashOutTaxRate,,,, JBCashOutHookSpecification[] memory returnedHook) =
210
- hook.beforeCashOutRecordedWith(beforeCashOutContext);
211
-
212
- // Check: does the cash out tax rate match the expected value?
213
- assertEq(cashOutTaxRate, JBConstants.MAX_CASH_OUT_TAX_RATE);
214
- // Check: does the returned hook address match the expected value?
215
- assertEq(address(returnedHook[0].hook), address(hook));
216
- }
217
-
218
- function test_beforeCashOutContext_revertIfNonZeroTokenCount(uint256 tokenCount) public {
219
- vm.assume(tokenCount > 0);
220
-
221
- // Expect a revert on account of the token count being non-zero while the total supply is zero.
222
- vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_UnexpectedTokenCashedOut.selector));
223
-
224
- hook.beforeCashOutRecordedWith(
225
- JBBeforeCashOutRecordedContext({
226
- terminal: address(0),
227
- holder: beneficiary,
228
- projectId: projectId,
229
- rulesetId: 0,
230
- cashOutCount: tokenCount,
231
- totalSupply: 0,
232
- surplus: JBTokenAmount({
233
- token: address(0), value: 100, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
234
- }),
235
- useTotalSurplus: true,
236
- cashOutTaxRate: 100,
237
- beneficiaryIsFeeless: false,
238
- metadata: new bytes(0)
239
- })
240
- );
241
- }
242
-
243
- function test_afterCashOutRecordedWith_burnCashOutNft(uint256 numberOfNfts) public {
244
- ForTest_JB721TiersHook hook = _initializeForTestHook(5);
245
-
246
- // Has to all fit in tier 1 (excluding reserve mints).
247
- numberOfNfts = bound(numberOfNfts, 1, 90);
248
-
249
- // Mock the directory call.
250
- mockAndExpect(
251
- address(mockJBDirectory),
252
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
253
- abi.encode(true)
254
- );
255
-
256
- uint256[] memory tokenList = new uint256[](numberOfNfts);
257
-
258
- bytes memory hookMetadata;
259
- bytes[] memory data;
260
- bytes4[] memory ids;
261
-
262
- for (uint256 i; i < numberOfNfts; i++) {
263
- uint16[] memory tierIdsToMint = new uint16[](1);
264
- tierIdsToMint[0] = 1;
265
-
266
- // Build the metadata using the tiers to mint and the overspending flag.
267
- data = new bytes[](1);
268
- data[0] = abi.encode(false, tierIdsToMint);
269
-
270
- // Pass the hook ID.
271
- ids = new bytes4[](1);
272
- ids[0] = metadataHelper.getId("pay", address(hook));
273
-
274
- // Generate the metadata.
275
- hookMetadata = metadataHelper.createMetadata(ids, data);
276
-
277
- // Mint the NFTs. Otherwise, the voting balance is not incremented,
278
- // which leads to an underflow upon cash out.
279
- vm.prank(mockTerminalAddress);
280
- JBAfterPayRecordedContext memory afterPayContext = JBAfterPayRecordedContext({
281
- payer: beneficiary,
282
- projectId: projectId,
283
- rulesetId: 0,
284
- amount: JBTokenAmount({
285
- token: JBConstants.NATIVE_TOKEN,
286
- value: 10,
287
- decimals: 18,
288
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
289
- }),
290
- forwardedAmount: JBTokenAmount({
291
- token: JBConstants.NATIVE_TOKEN,
292
- value: 0,
293
- decimals: 18,
294
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
295
- }), // 0
296
- // Forward to the hook.
297
- weight: 10 ** 18,
298
- newlyIssuedTokenCount: 0,
299
- beneficiary: beneficiary,
300
- hookMetadata: new bytes(0),
301
- payerMetadata: hookMetadata
302
- });
303
-
304
- hook.afterPayRecordedWith(afterPayContext);
305
-
306
- tokenList[i] = _generateTokenId(1, i + 1);
307
-
308
- // Check: was a new NFT minted?
309
- assertEq(hook.balanceOf(beneficiary), i + 1);
310
- }
311
-
312
- // Build the metadata with the tiers to cash out.
313
- data = new bytes[](1);
314
- data[0] = abi.encode(tokenList);
315
-
316
- // Pass the hook ID.
317
- ids = new bytes4[](1);
318
- ids[0] = metadataHelper.getId("cashOut", address(hook));
319
-
320
- // Generate the metadata.
321
- hookMetadata = metadataHelper.createMetadata(ids, data);
322
-
323
- vm.prank(mockTerminalAddress);
324
- hook.afterCashOutRecordedWith(
325
- JBAfterCashOutRecordedContext({
326
- holder: beneficiary,
327
- projectId: projectId,
328
- rulesetId: 1,
329
- cashOutCount: 0,
330
- reclaimedAmount: JBTokenAmount({
331
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
332
- }),
333
- forwardedAmount: JBTokenAmount({
334
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
335
- }), // 0, forwarded to the hook.
336
- cashOutTaxRate: 5000,
337
- beneficiary: payable(beneficiary),
338
- hookMetadata: bytes(""),
339
- cashOutMetadata: hookMetadata
340
- })
341
- );
342
-
343
- // Check: is the beneficiary's balance zero again?
344
- assertEq(hook.balanceOf(beneficiary), 0);
345
-
346
- // Check: was the number of burned NFTs recorded correctly (to match `numberOfNfts` in the first tier)?
347
- assertEq(hook.test_store().numberOfBurnedFor(address(hook), 1), numberOfNfts);
348
- }
349
-
350
- function test_afterCashOutRecordedWith_revertIfNotCorrectProjectId(uint8 wrongProjectId) public {
351
- vm.assume(wrongProjectId != projectId);
352
-
353
- uint256[] memory tokenList = new uint256[](1);
354
- tokenList[0] = 1;
355
-
356
- // Mock the directory call.
357
- mockAndExpect(
358
- address(mockJBDirectory),
359
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
360
- abi.encode(true)
361
- );
362
-
363
- // Expect to revert on account of the project ID being incorrect.
364
- vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidCashOut.selector));
365
-
366
- vm.prank(mockTerminalAddress);
367
- hook.afterCashOutRecordedWith(
368
- JBAfterCashOutRecordedContext({
369
- holder: beneficiary,
370
- projectId: wrongProjectId,
371
- rulesetId: 1,
372
- cashOutCount: 0,
373
- reclaimedAmount: JBTokenAmount({
374
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
375
- }),
376
- forwardedAmount: JBTokenAmount({
377
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
378
- }), // 0, forwarded to the hook.
379
- cashOutTaxRate: 5000,
380
- beneficiary: payable(beneficiary),
381
- hookMetadata: bytes(""),
382
- cashOutMetadata: abi.encode(type(IJB721TiersHook).interfaceId, tokenList)
383
- })
384
- );
385
- }
386
-
387
- function test_afterCashOutRecordedWith_revertIfCallerIsNotATerminalOfTheProject() public {
388
- uint256[] memory tokenList = new uint256[](1);
389
- tokenList[0] = 1;
390
-
391
- // Mock the directory call.
392
- mockAndExpect(
393
- address(mockJBDirectory),
394
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
395
- abi.encode(false)
396
- );
397
-
398
- // Expect to revert on account of the caller not being a terminal of the project.
399
- vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_InvalidCashOut.selector));
400
-
401
- vm.prank(mockTerminalAddress);
402
- hook.afterCashOutRecordedWith(
403
- JBAfterCashOutRecordedContext({
404
- holder: beneficiary,
405
- projectId: projectId,
406
- rulesetId: 1,
407
- cashOutCount: 0,
408
- reclaimedAmount: JBTokenAmount({
409
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
410
- }),
411
- forwardedAmount: JBTokenAmount({
412
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
413
- }), // 0, forwarded to the hook.
414
- cashOutTaxRate: 5000,
415
- beneficiary: payable(beneficiary),
416
- hookMetadata: bytes(""),
417
- cashOutMetadata: abi.encode(type(IJB721TiersHook).interfaceId, tokenList)
418
- })
419
- );
420
- }
421
-
422
- function test_afterCashOutRecordedWith_revertIfWrongHolder(address wrongHolder, uint8 tokenId) public {
423
- vm.assume(beneficiary != wrongHolder);
424
- vm.assume(tokenId != 0);
425
-
426
- ForTest_JB721TiersHook hook = _initializeForTestHook(1);
427
-
428
- hook.ForTest_setOwnerOf(tokenId, beneficiary);
429
-
430
- uint256[] memory tokenList = new uint256[](1);
431
- tokenList[0] = tokenId;
432
-
433
- // Build the metadata with the tiers to cash out.
434
- bytes[] memory data = new bytes[](1);
435
- data[0] = abi.encode(tokenList);
436
-
437
- // Pass the hook ID.
438
- bytes4[] memory ids = new bytes4[](1);
439
- ids[0] = metadataHelper.getId("cashOut", address(hook));
440
-
441
- // Generate the metadata.
442
- bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
443
-
444
- // Mock the directory call.
445
- mockAndExpect(
446
- address(mockJBDirectory),
447
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
448
- abi.encode(true)
449
- );
450
-
451
- vm.expectRevert(abi.encodeWithSelector(JB721Hook.JB721Hook_UnauthorizedToken.selector, tokenId, wrongHolder));
452
-
453
- vm.prank(mockTerminalAddress);
454
- hook.afterCashOutRecordedWith(
455
- JBAfterCashOutRecordedContext({
456
- holder: wrongHolder,
457
- projectId: projectId,
458
- rulesetId: 1,
459
- cashOutCount: 0,
460
- reclaimedAmount: JBTokenAmount({
461
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
462
- }),
463
- forwardedAmount: JBTokenAmount({
464
- token: address(0), value: 0, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
465
- }), // 0, forwarded to the hook.
466
- cashOutTaxRate: 5000,
467
- beneficiary: payable(wrongHolder),
468
- hookMetadata: bytes(""),
469
- cashOutMetadata: hookMetadata
470
- })
471
- );
472
- }
473
- }
@@ -1,182 +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
- /// @notice Tests that the 721 hook resolves the relay beneficiary from payment metadata.
8
- contract Test_relayBeneficiary_Unit is UnitTestSetup {
9
- /// @notice The metadata ID the 721 hook uses to look up the relay beneficiary.
10
- /// Must match `_721_BENEFICIARY_METADATA_ID` in JB721TiersHook.
11
- /// @notice Must match `BENEFICIARY_METADATA_ID` in JB721TiersHook.
12
- bytes4 constant BENEFICIARY_METADATA_ID = bytes4(keccak256("JB_721_BENEFICIARY"));
13
-
14
- address relayUser = makeAddr("relayUser");
15
- address sucker = makeAddr("sucker");
16
-
17
- function setUp() public override {
18
- super.setUp();
19
-
20
- // Mock directory: terminal is valid for all calls.
21
- vm.mockCall(
22
- address(mockJBDirectory),
23
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
24
- abi.encode(true)
25
- );
26
- }
27
-
28
- /// @notice When metadata contains a relay beneficiary, the spec should use that address (not context.beneficiary).
29
- function test_beforePay_relayBeneficiary_usedInSpec() public {
30
- JB721TiersHook tiersHook = _initHookDefaultTiers(3);
31
-
32
- // Build metadata with tier selection + relay beneficiary.
33
- uint16[] memory tierIdsToMint = new uint16[](1);
34
- tierIdsToMint[0] = 1;
35
-
36
- // Encode tier selection metadata (for the hook's own metadata ID).
37
- bytes memory tierData = abi.encode(false, tierIdsToMint, new bytes[](0));
38
-
39
- // Build metadata with two entries: hook tier data + relay beneficiary.
40
- bytes4[] memory ids = new bytes4[](2);
41
- bytes[] memory datas = new bytes[](2);
42
-
43
- ids[0] = JBMetadataResolver.getId({purpose: "pay", target: address(tiersHook)});
44
- datas[0] = tierData;
45
- ids[1] = BENEFICIARY_METADATA_ID;
46
- datas[1] = abi.encode(relayUser);
47
-
48
- bytes memory metadata = metadataHelper.createMetadata(ids, datas);
49
-
50
- // Build pay context with sucker as beneficiary (simulating cross-chain payment).
51
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
52
- terminal: mockTerminalAddress,
53
- payer: sucker,
54
- amount: JBTokenAmount({
55
- token: JBConstants.NATIVE_TOKEN,
56
- value: 10,
57
- decimals: 18,
58
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
59
- }),
60
- projectId: projectId,
61
- rulesetId: block.timestamp,
62
- beneficiary: sucker,
63
- weight: 10e18,
64
- reservedPercent: 5000,
65
- metadata: metadata
66
- });
67
-
68
- (, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
69
-
70
- // The spec's metadata should encode relayUser as the beneficiary, not sucker.
71
- assertEq(specs.length, 1, "should return one spec");
72
- (address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
73
- assertEq(specBeneficiary, relayUser, "spec should use relay beneficiary, not context.beneficiary");
74
- }
75
-
76
- /// @notice When no relay metadata is present, the spec should use context.beneficiary as-is.
77
- function test_beforePay_noRelayMetadata_usesContextBeneficiary() public {
78
- JB721TiersHook tiersHook = _initHookDefaultTiers(3);
79
-
80
- // Build metadata with only tier selection (no relay beneficiary).
81
- uint16[] memory tierIdsToMint = new uint16[](1);
82
- tierIdsToMint[0] = 1;
83
- bytes memory tierData = abi.encode(false, tierIdsToMint, new bytes[](0));
84
-
85
- bytes4[] memory ids = new bytes4[](1);
86
- bytes[] memory datas = new bytes[](1);
87
- ids[0] = JBMetadataResolver.getId({purpose: "pay", target: address(tiersHook)});
88
- datas[0] = tierData;
89
-
90
- bytes memory metadata = metadataHelper.createMetadata(ids, datas);
91
-
92
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
93
- terminal: mockTerminalAddress,
94
- payer: makeAddr("payer"),
95
- amount: JBTokenAmount({
96
- token: JBConstants.NATIVE_TOKEN,
97
- value: 10,
98
- decimals: 18,
99
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
100
- }),
101
- projectId: projectId,
102
- rulesetId: block.timestamp,
103
- beneficiary: beneficiary,
104
- weight: 10e18,
105
- reservedPercent: 5000,
106
- metadata: metadata
107
- });
108
-
109
- (, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
110
-
111
- assertEq(specs.length, 1, "should return one spec");
112
- (address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
113
- assertEq(specBeneficiary, beneficiary, "spec should use context.beneficiary when no relay metadata");
114
- }
115
-
116
- /// @notice When relay metadata is present but the address is zero, fall back to context.beneficiary.
117
- function test_beforePay_relayBeneficiaryZero_fallsBackToContext() public {
118
- JB721TiersHook tiersHook = _initHookDefaultTiers(3);
119
-
120
- uint16[] memory tierIdsToMint = new uint16[](1);
121
- tierIdsToMint[0] = 1;
122
- bytes memory tierData = abi.encode(false, tierIdsToMint, new bytes[](0));
123
-
124
- bytes4[] memory ids = new bytes4[](2);
125
- bytes[] memory datas = new bytes[](2);
126
- ids[0] = JBMetadataResolver.getId({purpose: "pay", target: address(tiersHook)});
127
- datas[0] = tierData;
128
- ids[1] = BENEFICIARY_METADATA_ID;
129
- datas[1] = abi.encode(address(0));
130
-
131
- bytes memory metadata = metadataHelper.createMetadata(ids, datas);
132
-
133
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
134
- terminal: mockTerminalAddress,
135
- payer: sucker,
136
- amount: JBTokenAmount({
137
- token: JBConstants.NATIVE_TOKEN,
138
- value: 10,
139
- decimals: 18,
140
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
141
- }),
142
- projectId: projectId,
143
- rulesetId: block.timestamp,
144
- beneficiary: beneficiary,
145
- weight: 10e18,
146
- reservedPercent: 5000,
147
- metadata: metadata
148
- });
149
-
150
- (, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
151
-
152
- (address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
153
- assertEq(specBeneficiary, beneficiary, "zero relay address should fall back to context.beneficiary");
154
- }
155
-
156
- /// @notice When metadata is empty, use context.beneficiary.
157
- function test_beforePay_emptyMetadata_usesContextBeneficiary() public {
158
- JB721TiersHook tiersHook = _initHookDefaultTiers(3);
159
-
160
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
161
- terminal: mockTerminalAddress,
162
- payer: makeAddr("payer"),
163
- amount: JBTokenAmount({
164
- token: JBConstants.NATIVE_TOKEN,
165
- value: 10,
166
- decimals: 18,
167
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
168
- }),
169
- projectId: projectId,
170
- rulesetId: block.timestamp,
171
- beneficiary: beneficiary,
172
- weight: 10e18,
173
- reservedPercent: 5000,
174
- metadata: ""
175
- });
176
-
177
- (, JBPayHookSpecification[] memory specs) = tiersHook.beforePayRecordedWith(context);
178
-
179
- (address specBeneficiary,,) = abi.decode(specs[0].metadata, (address, address, bytes));
180
- assertEq(specBeneficiary, beneficiary, "empty metadata should use context.beneficiary");
181
- }
182
- }