@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.
- package/USER_JOURNEYS.md +11 -0
- package/package.json +3 -3
- package/script/Deploy.s.sol +53 -19
- package/src/JB721Checkpoints.sol +92 -0
- package/src/JB721CheckpointsDeployer.sol +45 -0
- package/src/JB721TiersHook.sol +90 -116
- package/src/abstract/JB721Hook.sol +5 -0
- package/src/interfaces/IJB721Checkpoints.sol +34 -0
- package/src/interfaces/IJB721CheckpointsDeployer.sol +20 -0
- package/src/interfaces/IJB721TiersHook.sol +8 -0
- package/src/libraries/JB721Constants.sol +6 -0
- package/src/libraries/JB721TiersHookLib.sol +353 -146
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +11 -1
- package/test/Fork.t.sol +11 -2
- package/test/TestAuditGaps.sol +1 -1
- package/test/TestCheckpoints.t.sol +329 -0
- package/test/audit/CodexNemesisRepoFindings.t.sol +270 -0
- package/test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol +161 -0
- package/test/audit/CodexSplitCreditsMismatch.t.sol +2 -1
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +1 -0
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +249 -0
- package/test/audit/SplitFailureRedistribution.t.sol +2 -1
- package/test/fork/ERC20CashOutFork.t.sol +11 -2
- package/test/fork/ERC20TierSplitFork.t.sol +11 -2
- package/test/fork/IssueTokensForSplitsFork.t.sol +11 -2
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +2 -2
- package/test/regression/SplitDistributionBugs.t.sol +5 -5
- package/test/regression/SplitNoBeneficiary.t.sol +1 -1
- package/test/unit/AuditFixes_Unit.t.sol +5 -5
- package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -0
- package/test/unit/pay_Unit.t.sol +1 -0
- package/test/unit/redeem_Unit.t.sol +3 -3
- package/test/unit/relayBeneficiary_Unit.t.sol +182 -0
- package/test/unit/splitHookDistribution_Unit.t.sol +6 -6
- 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
|
|
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
|
|
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
|
|
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
|
}
|