@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,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
- }