@bananapus/721-hook-v6 0.0.32 → 0.0.34

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 (35) hide show
  1. package/USER_JOURNEYS.md +11 -0
  2. package/package.json +3 -3
  3. package/script/Deploy.s.sol +53 -19
  4. package/src/JB721Checkpoints.sol +92 -0
  5. package/src/JB721CheckpointsDeployer.sol +45 -0
  6. package/src/JB721TiersHook.sol +90 -116
  7. package/src/abstract/JB721Hook.sol +5 -0
  8. package/src/interfaces/IJB721Checkpoints.sol +34 -0
  9. package/src/interfaces/IJB721CheckpointsDeployer.sol +20 -0
  10. package/src/interfaces/IJB721TiersHook.sol +8 -0
  11. package/src/libraries/JB721Constants.sol +6 -0
  12. package/src/libraries/JB721TiersHookLib.sol +353 -146
  13. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +11 -1
  14. package/test/Fork.t.sol +11 -2
  15. package/test/TestAuditGaps.sol +1 -1
  16. package/test/TestCheckpoints.t.sol +329 -0
  17. package/test/audit/CodexNemesisRepoFindings.t.sol +270 -0
  18. package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +161 -0
  19. package/test/audit/CodexSplitCreditsMismatch.t.sol +2 -1
  20. package/test/audit/CrossCurrencySplitNoPrices.t.sol +1 -0
  21. package/test/audit/SameCurrencyDecimalMismatch.t.sol +249 -0
  22. package/test/audit/SplitFailureRedistribution.t.sol +2 -1
  23. package/test/fork/ERC20CashOutFork.t.sol +11 -2
  24. package/test/fork/ERC20TierSplitFork.t.sol +11 -2
  25. package/test/fork/IssueTokensForSplitsFork.t.sol +11 -2
  26. package/test/regression/BrokenTerminalDoesNotDos.t.sol +2 -2
  27. package/test/regression/SplitDistributionBugs.t.sol +5 -5
  28. package/test/regression/SplitNoBeneficiary.t.sol +1 -1
  29. package/test/unit/AuditFixes_Unit.t.sol +5 -5
  30. package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -0
  31. package/test/unit/pay_Unit.t.sol +1 -0
  32. package/test/unit/redeem_Unit.t.sol +3 -3
  33. package/test/unit/relayBeneficiary_Unit.t.sol +182 -0
  34. package/test/unit/splitHookDistribution_Unit.t.sol +6 -6
  35. package/test/unit/tierSplitRouting_Unit.t.sol +2 -2
@@ -54,7 +54,7 @@ contract Test_cashOut_Unit is UnitTestSetup {
54
54
 
55
55
  // Generate the metadata.
56
56
  bytes memory hookMetadata = metadataHelper.createMetadata(ids, data);
57
- (uint256 cashOutTaxRate,,, JBCashOutHookSpecification[] memory returnedHook) = hook.beforeCashOutRecordedWith(
57
+ (uint256 cashOutTaxRate,,,, JBCashOutHookSpecification[] memory returnedHook) = hook.beforeCashOutRecordedWith(
58
58
  JBBeforeCashOutRecordedContext({
59
59
  terminal: address(0),
60
60
  holder: beneficiary,
@@ -118,7 +118,7 @@ contract Test_cashOut_Unit is UnitTestSetup {
118
118
  weight += (i + 1) * (i + 1) * 10;
119
119
  }
120
120
 
121
- (cashOutTaxRate,,, returnedHook) = hook.beforeCashOutRecordedWith(
121
+ (cashOutTaxRate,,,, returnedHook) = hook.beforeCashOutRecordedWith(
122
122
  JBBeforeCashOutRecordedContext({
123
123
  terminal: address(0),
124
124
  holder: beneficiary,
@@ -206,7 +206,7 @@ contract Test_cashOut_Unit is UnitTestSetup {
206
206
  metadata: hookMetadata
207
207
  });
208
208
 
209
- (uint256 cashOutTaxRate,,, JBCashOutHookSpecification[] memory returnedHook) =
209
+ (uint256 cashOutTaxRate,,,, JBCashOutHookSpecification[] memory returnedHook) =
210
210
  hook.beforeCashOutRecordedWith(beforeCashOutContext);
211
211
 
212
212
  // Check: does the cash out tax rate match the expected value?
@@ -0,0 +1,182 @@
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
+ }
@@ -169,7 +169,7 @@ contract Test_SplitHookDistribution is UnitTestSetup {
169
169
  weight: 10e18,
170
170
  newlyIssuedTokenCount: 0,
171
171
  beneficiary: beneficiary,
172
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
172
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
173
173
  payerMetadata: payerMetadata
174
174
  });
175
175
 
@@ -260,7 +260,7 @@ contract Test_SplitHookDistribution is UnitTestSetup {
260
260
  weight: 10e18,
261
261
  newlyIssuedTokenCount: 0,
262
262
  beneficiary: beneficiary,
263
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
263
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
264
264
  payerMetadata: payerMetadata
265
265
  });
266
266
 
@@ -342,7 +342,7 @@ contract Test_SplitHookDistribution is UnitTestSetup {
342
342
  weight: 10e18,
343
343
  newlyIssuedTokenCount: 0,
344
344
  beneficiary: beneficiary,
345
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
345
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
346
346
  payerMetadata: payerMetadata
347
347
  });
348
348
 
@@ -429,7 +429,7 @@ contract Test_SplitHookDistribution is UnitTestSetup {
429
429
  weight: 10e18,
430
430
  newlyIssuedTokenCount: 0,
431
431
  beneficiary: beneficiary,
432
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
432
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
433
433
  payerMetadata: payerMetadata
434
434
  });
435
435
 
@@ -512,7 +512,7 @@ contract Test_SplitHookDistribution is UnitTestSetup {
512
512
  weight: 10e18,
513
513
  newlyIssuedTokenCount: 0,
514
514
  beneficiary: beneficiary,
515
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
515
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
516
516
  payerMetadata: payerMetadata
517
517
  });
518
518
 
@@ -586,7 +586,7 @@ contract Test_SplitHookDistribution is UnitTestSetup {
586
586
  weight: 10e18,
587
587
  newlyIssuedTokenCount: 0,
588
588
  beneficiary: beneficiary,
589
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
589
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
590
590
  payerMetadata: payerMetadata
591
591
  });
592
592
 
@@ -377,7 +377,7 @@ contract Test_TierSplitRouting is UnitTestSetup {
377
377
  weight: 10e18,
378
378
  newlyIssuedTokenCount: 0,
379
379
  beneficiary: beneficiary,
380
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
380
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
381
381
  payerMetadata: payerMetadata
382
382
  });
383
383
 
@@ -570,7 +570,7 @@ contract Test_TierSplitRouting is UnitTestSetup {
570
570
  weight: 10e18,
571
571
  newlyIssuedTokenCount: 0,
572
572
  beneficiary: beneficiary,
573
- hookMetadata: abi.encode(splitTierIds, splitAmounts),
573
+ hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
574
574
  payerMetadata: payerMetadata
575
575
  });
576
576
  }