@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.
- package/foundry.lock +1 -7
- package/foundry.toml +1 -1
- package/package.json +20 -9
- package/script/Deploy.s.sol +2 -2
- package/src/JB721Checkpoints.sol +60 -18
- package/src/JB721CheckpointsDeployer.sol +10 -5
- package/src/JB721TiersHook.sol +4 -1
- package/src/JB721TiersHookProjectDeployer.sol +68 -30
- package/src/JB721TiersHookStore.sol +1 -4
- package/src/interfaces/IJB721Checkpoints.sol +21 -14
- package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
- package/test/utils/AccessJBLib.sol +49 -0
- package/test/utils/ForTest_JB721TiersHook.sol +246 -0
- package/test/utils/TestBaseWorkflow.sol +213 -0
- package/test/utils/UnitTestSetup.sol +805 -0
- package/.gas-snapshot +0 -152
- package/ADMINISTRATION.md +0 -87
- package/ARCHITECTURE.md +0 -98
- package/AUDIT_INSTRUCTIONS.md +0 -77
- package/RISKS.md +0 -118
- package/SKILLS.md +0 -43
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -121
- package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
- package/slither-ci.config.json +0 -10
- package/test/721HookAttacks.t.sol +0 -408
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
- package/test/Fork.t.sol +0 -2346
- package/test/TestAuditGaps.sol +0 -1075
- package/test/TestCheckpoints.t.sol +0 -341
- package/test/TestSafeTransferReentrancy.t.sol +0 -305
- package/test/TestVotingUnitsLifecycle.t.sol +0 -313
- package/test/audit/AuditRegressions.t.sol +0 -83
- package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
- package/test/audit/FreshAudit.t.sol +0 -197
- package/test/audit/FutureTierPoC.t.sol +0 -39
- package/test/audit/FutureTierRemoval.t.sol +0 -47
- package/test/audit/Pass12L18.t.sol +0 -80
- package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
- package/test/audit/ProjectDeployerAuth.t.sol +0 -266
- package/test/audit/RepoFindings.t.sol +0 -195
- package/test/audit/ReserveActivation.t.sol +0 -87
- package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
- package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
- package/test/audit/SplitCreditsMismatch.t.sol +0 -219
- package/test/audit/SplitFailureRedistribution.t.sol +0 -143
- package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
- package/test/fork/ERC20CashOutFork.t.sol +0 -633
- package/test/fork/ERC20TierSplitFork.t.sol +0 -596
- package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
- package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
- package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
- package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
- package/test/invariants/handlers/TierStoreHandler.sol +0 -165
- package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
- package/test/regression/CacheTierLookup.t.sol +0 -190
- package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
- package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
- package/test/regression/SplitDistributionBugs.t.sol +0 -751
- package/test/regression/SplitNoBeneficiary.t.sol +0 -140
- package/test/unit/AuditFixes_Unit.t.sol +0 -624
- package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
- package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
- package/test/unit/JBBitmap.t.sol +0 -170
- package/test/unit/JBIpfsDecoder.t.sol +0 -136
- package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
- package/test/unit/adjustTier_Unit.t.sol +0 -1942
- package/test/unit/deployer_Unit.t.sol +0 -114
- package/test/unit/getters_constructor_Unit.t.sol +0 -593
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
- package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
- package/test/unit/pay_Unit.t.sol +0 -1661
- package/test/unit/redeem_Unit.t.sol +0 -473
- package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
- package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
- package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
|
@@ -1,249 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
import {UnitTestSetup} from "../utils/UnitTestSetup.sol";
|
|
5
|
-
import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
|
|
6
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
7
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
8
|
-
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
9
|
-
import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
|
|
10
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
11
|
-
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
12
|
-
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
13
|
-
import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
|
|
14
|
-
import {JB721TiersHook} from "../../src/JB721TiersHook.sol";
|
|
15
|
-
import {JB721CheckpointsDeployer} from "../../src/JB721CheckpointsDeployer.sol";
|
|
16
|
-
import {IJB721CheckpointsDeployer} from "../../src/interfaces/IJB721CheckpointsDeployer.sol";
|
|
17
|
-
import {JB721TierConfig} from "../../src/structs/JB721TierConfig.sol";
|
|
18
|
-
import {JB721InitTiersConfig} from "../../src/structs/JB721InitTiersConfig.sol";
|
|
19
|
-
import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
|
|
20
|
-
|
|
21
|
-
/// @notice Regression test for: same-currency decimal mismatch in split forwarding.
|
|
22
|
-
/// @dev When pricing decimals differ from payment decimals but the currency is the same,
|
|
23
|
-
/// `convertAndCapSplitAmounts` must rescale split amounts before comparing to `amountValue`.
|
|
24
|
-
/// Without the fix, split amounts stay in pricing decimals (e.g. 18), the cap comparison uses
|
|
25
|
-
/// payment decimals (e.g. 6), and the cap clips the split to 100% of the payment.
|
|
26
|
-
contract SameCurrencyDecimalMismatch is UnitTestSetup {
|
|
27
|
-
// Shared constants.
|
|
28
|
-
address constant MOCK_TOKEN = address(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
|
|
29
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
30
|
-
uint32 constant CURRENCY = uint32(uint160(MOCK_TOKEN));
|
|
31
|
-
|
|
32
|
-
/// @notice Prove that a 50% split with same currency but different decimals (pricing=18, payment=6)
|
|
33
|
-
/// correctly forwards ~50% of the payment, not 100%.
|
|
34
|
-
function test_sameCurrency_differentDecimals_splitAmountScaledCorrectly() public {
|
|
35
|
-
// Deploy hook with PRICES=address(0), tier priced at 1e18 (18-decimal), 50% split.
|
|
36
|
-
JB721TiersHook testHook;
|
|
37
|
-
{
|
|
38
|
-
JB721TiersHook origin = new JB721TiersHook(
|
|
39
|
-
IJBDirectory(mockJBDirectory),
|
|
40
|
-
IJBPermissions(mockJBPermissions),
|
|
41
|
-
IJBPrices(address(0)),
|
|
42
|
-
IJBRulesets(mockJBRulesets),
|
|
43
|
-
store,
|
|
44
|
-
IJBSplits(mockJBSplits),
|
|
45
|
-
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
46
|
-
trustedForwarder
|
|
47
|
-
);
|
|
48
|
-
address hookAddr = makeAddr("hook18to6");
|
|
49
|
-
vm.etch(hookAddr, address(origin).code);
|
|
50
|
-
testHook = JB721TiersHook(hookAddr);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
{
|
|
54
|
-
(JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
|
|
55
|
-
tierConfigs[0].price = 1e18;
|
|
56
|
-
tierConfigs[0].splitPercent = 500_000_000; // 50%.
|
|
57
|
-
testHook.initialize(
|
|
58
|
-
projectId,
|
|
59
|
-
name,
|
|
60
|
-
symbol,
|
|
61
|
-
baseUri,
|
|
62
|
-
IJB721TokenUriResolver(mockTokenUriResolver),
|
|
63
|
-
contractUri,
|
|
64
|
-
JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
|
|
65
|
-
JB721TiersHookFlags({
|
|
66
|
-
preventOverspending: false,
|
|
67
|
-
issueTokensForSplits: false,
|
|
68
|
-
noNewTiersWithReserves: false,
|
|
69
|
-
noNewTiersWithVotes: false,
|
|
70
|
-
noNewTiersWithOwnerMinting: false
|
|
71
|
-
})
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Build payer metadata requesting tier 1.
|
|
76
|
-
bytes memory payerMetadata;
|
|
77
|
-
{
|
|
78
|
-
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
79
|
-
tierIdsToMint[0] = 1;
|
|
80
|
-
bytes[] memory data = new bytes[](1);
|
|
81
|
-
data[0] = abi.encode(true, tierIdsToMint);
|
|
82
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
83
|
-
ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
|
|
84
|
-
payerMetadata = metadataHelper.createMetadata(ids, data);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Pay 1.0 token reported as 6 decimals (value = 1e6). Same currency, different decimals.
|
|
88
|
-
(uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
|
|
89
|
-
JBBeforePayRecordedContext({
|
|
90
|
-
terminal: mockTerminalAddress,
|
|
91
|
-
payer: beneficiary,
|
|
92
|
-
amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e6, decimals: 6, currency: CURRENCY}),
|
|
93
|
-
projectId: projectId,
|
|
94
|
-
rulesetId: 0,
|
|
95
|
-
beneficiary: beneficiary,
|
|
96
|
-
weight: 10e18,
|
|
97
|
-
reservedPercent: 0,
|
|
98
|
-
metadata: payerMetadata
|
|
99
|
-
})
|
|
100
|
-
);
|
|
101
|
-
|
|
102
|
-
// Without the fix: split amount (5e17 in 18-decimal pricing) is compared to amountValue (1e6),
|
|
103
|
-
// causing the cap to clip it to 1e6 (100% of payment) and weight becomes 0.
|
|
104
|
-
// With the fix: split is rescaled to 5e5 (50% of 1e6) and weight is 5e18.
|
|
105
|
-
assertEq(hookSpecs[0].amount, 5e5, "split should be 50% of payment (5e5), not capped to 100%");
|
|
106
|
-
assertEq(weight, 5e18, "weight should be 50% (half goes to splits)");
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/// @notice Sanity check: same currency AND same decimals — no rescaling needed.
|
|
110
|
-
function test_sameCurrency_sameDecimals_splitAmountUnchanged() public {
|
|
111
|
-
JB721TiersHook testHook;
|
|
112
|
-
{
|
|
113
|
-
JB721TiersHook origin = new JB721TiersHook(
|
|
114
|
-
IJBDirectory(mockJBDirectory),
|
|
115
|
-
IJBPermissions(mockJBPermissions),
|
|
116
|
-
IJBPrices(address(0)),
|
|
117
|
-
IJBRulesets(mockJBRulesets),
|
|
118
|
-
store,
|
|
119
|
-
IJBSplits(mockJBSplits),
|
|
120
|
-
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
121
|
-
trustedForwarder
|
|
122
|
-
);
|
|
123
|
-
address hookAddr = makeAddr("hook18to18");
|
|
124
|
-
vm.etch(hookAddr, address(origin).code);
|
|
125
|
-
testHook = JB721TiersHook(hookAddr);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
{
|
|
129
|
-
(JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
|
|
130
|
-
tierConfigs[0].price = 1e18;
|
|
131
|
-
tierConfigs[0].splitPercent = 500_000_000;
|
|
132
|
-
testHook.initialize(
|
|
133
|
-
projectId,
|
|
134
|
-
name,
|
|
135
|
-
symbol,
|
|
136
|
-
baseUri,
|
|
137
|
-
IJB721TokenUriResolver(mockTokenUriResolver),
|
|
138
|
-
contractUri,
|
|
139
|
-
JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 18}),
|
|
140
|
-
JB721TiersHookFlags({
|
|
141
|
-
preventOverspending: false,
|
|
142
|
-
issueTokensForSplits: false,
|
|
143
|
-
noNewTiersWithReserves: false,
|
|
144
|
-
noNewTiersWithVotes: false,
|
|
145
|
-
noNewTiersWithOwnerMinting: false
|
|
146
|
-
})
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
bytes memory payerMetadata;
|
|
151
|
-
{
|
|
152
|
-
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
153
|
-
tierIdsToMint[0] = 1;
|
|
154
|
-
bytes[] memory data = new bytes[](1);
|
|
155
|
-
data[0] = abi.encode(true, tierIdsToMint);
|
|
156
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
157
|
-
ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
|
|
158
|
-
payerMetadata = metadataHelper.createMetadata(ids, data);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
(uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
|
|
162
|
-
JBBeforePayRecordedContext({
|
|
163
|
-
terminal: mockTerminalAddress,
|
|
164
|
-
payer: beneficiary,
|
|
165
|
-
amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
|
|
166
|
-
projectId: projectId,
|
|
167
|
-
rulesetId: 0,
|
|
168
|
-
beneficiary: beneficiary,
|
|
169
|
-
weight: 10e18,
|
|
170
|
-
reservedPercent: 0,
|
|
171
|
-
metadata: payerMetadata
|
|
172
|
-
})
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
assertEq(hookSpecs[0].amount, 5e17, "split should be 50% of payment");
|
|
176
|
-
assertEq(weight, 5e18, "weight should be 50%");
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/// @notice Same currency, payment has MORE decimals than pricing (pricing=6, payment=18).
|
|
180
|
-
function test_sameCurrency_paymentMoreDecimals_splitScaledUp() public {
|
|
181
|
-
JB721TiersHook testHook;
|
|
182
|
-
{
|
|
183
|
-
JB721TiersHook origin = new JB721TiersHook(
|
|
184
|
-
IJBDirectory(mockJBDirectory),
|
|
185
|
-
IJBPermissions(mockJBPermissions),
|
|
186
|
-
IJBPrices(address(0)),
|
|
187
|
-
IJBRulesets(mockJBRulesets),
|
|
188
|
-
store,
|
|
189
|
-
IJBSplits(mockJBSplits),
|
|
190
|
-
IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
|
|
191
|
-
trustedForwarder
|
|
192
|
-
);
|
|
193
|
-
address hookAddr = makeAddr("hook6to18");
|
|
194
|
-
vm.etch(hookAddr, address(origin).code);
|
|
195
|
-
testHook = JB721TiersHook(hookAddr);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
{
|
|
199
|
-
(JB721TierConfig[] memory tierConfigs,) = _createTiers(defaultTierConfig, 1);
|
|
200
|
-
tierConfigs[0].price = 1e6; // 1.0 token in 6-decimal pricing.
|
|
201
|
-
tierConfigs[0].splitPercent = 500_000_000;
|
|
202
|
-
testHook.initialize(
|
|
203
|
-
projectId,
|
|
204
|
-
name,
|
|
205
|
-
symbol,
|
|
206
|
-
baseUri,
|
|
207
|
-
IJB721TokenUriResolver(mockTokenUriResolver),
|
|
208
|
-
contractUri,
|
|
209
|
-
JB721InitTiersConfig({tiers: tierConfigs, currency: CURRENCY, decimals: 6}),
|
|
210
|
-
JB721TiersHookFlags({
|
|
211
|
-
preventOverspending: false,
|
|
212
|
-
issueTokensForSplits: false,
|
|
213
|
-
noNewTiersWithReserves: false,
|
|
214
|
-
noNewTiersWithVotes: false,
|
|
215
|
-
noNewTiersWithOwnerMinting: false
|
|
216
|
-
})
|
|
217
|
-
);
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
bytes memory payerMetadata;
|
|
221
|
-
{
|
|
222
|
-
uint16[] memory tierIdsToMint = new uint16[](1);
|
|
223
|
-
tierIdsToMint[0] = 1;
|
|
224
|
-
bytes[] memory data = new bytes[](1);
|
|
225
|
-
data[0] = abi.encode(true, tierIdsToMint);
|
|
226
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
227
|
-
ids[0] = metadataHelper.getId("pay", testHook.METADATA_ID_TARGET());
|
|
228
|
-
payerMetadata = metadataHelper.createMetadata(ids, data);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
(uint256 weight, JBPayHookSpecification[] memory hookSpecs) = testHook.beforePayRecordedWith(
|
|
232
|
-
JBBeforePayRecordedContext({
|
|
233
|
-
terminal: mockTerminalAddress,
|
|
234
|
-
payer: beneficiary,
|
|
235
|
-
amount: JBTokenAmount({token: MOCK_TOKEN, value: 1e18, decimals: 18, currency: CURRENCY}),
|
|
236
|
-
projectId: projectId,
|
|
237
|
-
rulesetId: 0,
|
|
238
|
-
beneficiary: beneficiary,
|
|
239
|
-
weight: 10e18,
|
|
240
|
-
reservedPercent: 0,
|
|
241
|
-
metadata: payerMetadata
|
|
242
|
-
})
|
|
243
|
-
);
|
|
244
|
-
|
|
245
|
-
// 50% of 1.0 token in 18-decimal payment = 5e17.
|
|
246
|
-
assertEq(hookSpecs[0].amount, 5e17, "split scaled up to 18-decimal payment");
|
|
247
|
-
assertEq(weight, 5e18, "weight should be 50%");
|
|
248
|
-
}
|
|
249
|
-
}
|
|
@@ -1,219 +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
|
-
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
8
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
9
|
-
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
10
|
-
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
11
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
12
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
13
|
-
import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
|
|
14
|
-
|
|
15
|
-
/// @notice Regression test: split metadata is proportionally scaled when credits fund a split-bearing tier mint.
|
|
16
|
-
/// @dev Previously (pre-fix), the per-tier split amounts were left at the uncapped value, trapping forwarded ETH.
|
|
17
|
-
/// After the F-2 fix, split amounts are scaled down to match the actual forwarded amount.
|
|
18
|
-
contract SplitCreditsMismatch is UnitTestSetup {
|
|
19
|
-
address internal splitBeneficiary = makeAddr("splitBeneficiary");
|
|
20
|
-
|
|
21
|
-
function setUp() public override {
|
|
22
|
-
super.setUp();
|
|
23
|
-
vm.etch(mockJBSplits, new bytes(0x69));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function _buildPayMetadata(
|
|
27
|
-
address hookAddress,
|
|
28
|
-
bool allowOverspending,
|
|
29
|
-
uint16[] memory tierIdsToMint
|
|
30
|
-
)
|
|
31
|
-
internal
|
|
32
|
-
view
|
|
33
|
-
returns (bytes memory)
|
|
34
|
-
{
|
|
35
|
-
bytes[] memory data = new bytes[](1);
|
|
36
|
-
data[0] = abi.encode(allowOverspending, tierIdsToMint);
|
|
37
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
38
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
39
|
-
return metadataHelper.createMetadata(ids, data);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function _beforePayContext(
|
|
43
|
-
address hookAddress,
|
|
44
|
-
uint256 amountValue,
|
|
45
|
-
uint16[] memory tierIdsToMint
|
|
46
|
-
)
|
|
47
|
-
internal
|
|
48
|
-
view
|
|
49
|
-
returns (JBBeforePayRecordedContext memory)
|
|
50
|
-
{
|
|
51
|
-
return JBBeforePayRecordedContext({
|
|
52
|
-
terminal: mockTerminalAddress,
|
|
53
|
-
payer: beneficiary,
|
|
54
|
-
amount: JBTokenAmount({
|
|
55
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
56
|
-
value: amountValue,
|
|
57
|
-
decimals: 18,
|
|
58
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
59
|
-
}),
|
|
60
|
-
projectId: projectId,
|
|
61
|
-
rulesetId: 0,
|
|
62
|
-
beneficiary: beneficiary,
|
|
63
|
-
weight: 10e18,
|
|
64
|
-
reservedPercent: 5000,
|
|
65
|
-
metadata: _buildPayMetadata(hookAddress, false, tierIdsToMint)
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function _afterPayContext(
|
|
70
|
-
address hookAddress,
|
|
71
|
-
uint256 amountValue,
|
|
72
|
-
uint256 forwardedAmountValue,
|
|
73
|
-
bytes memory hookMetadata,
|
|
74
|
-
uint16[] memory tierIdsToMint
|
|
75
|
-
)
|
|
76
|
-
internal
|
|
77
|
-
view
|
|
78
|
-
returns (JBAfterPayRecordedContext memory)
|
|
79
|
-
{
|
|
80
|
-
return JBAfterPayRecordedContext({
|
|
81
|
-
payer: beneficiary,
|
|
82
|
-
projectId: projectId,
|
|
83
|
-
rulesetId: 0,
|
|
84
|
-
amount: JBTokenAmount({
|
|
85
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
86
|
-
value: amountValue,
|
|
87
|
-
decimals: 18,
|
|
88
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
89
|
-
}),
|
|
90
|
-
forwardedAmount: JBTokenAmount({
|
|
91
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
92
|
-
value: forwardedAmountValue,
|
|
93
|
-
decimals: 18,
|
|
94
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
95
|
-
}),
|
|
96
|
-
weight: 10e18,
|
|
97
|
-
newlyIssuedTokenCount: 0,
|
|
98
|
-
beneficiary: beneficiary,
|
|
99
|
-
hookMetadata: hookMetadata,
|
|
100
|
-
payerMetadata: _buildPayMetadata(hookAddress, true, tierIdsToMint)
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function test_payCreditsScaleSplitMetadata_andForwardEth() public {
|
|
105
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
106
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
107
|
-
|
|
108
|
-
vm.mockCall(
|
|
109
|
-
mockJBDirectory,
|
|
110
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
111
|
-
abi.encode(true)
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
// Tier costs 1 ether and routes 100% of its effective price to splits.
|
|
115
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
116
|
-
tierConfigs[0] = JB721TierConfig({
|
|
117
|
-
price: 1 ether,
|
|
118
|
-
initialSupply: 100,
|
|
119
|
-
votingUnits: 0,
|
|
120
|
-
reserveFrequency: 0,
|
|
121
|
-
reserveBeneficiary: reserveBeneficiary,
|
|
122
|
-
encodedIPFSUri: bytes32(uint256(0x1234)),
|
|
123
|
-
category: 1,
|
|
124
|
-
discountPercent: 0,
|
|
125
|
-
flags: JB721TierConfigFlags({
|
|
126
|
-
allowOwnerMint: false,
|
|
127
|
-
useReserveBeneficiaryAsDefault: false,
|
|
128
|
-
transfersPausable: false,
|
|
129
|
-
useVotingUnits: false,
|
|
130
|
-
cantBeRemoved: false,
|
|
131
|
-
cantIncreaseDiscountPercent: false,
|
|
132
|
-
cantBuyWithCredits: false
|
|
133
|
-
}),
|
|
134
|
-
splitPercent: 1_000_000_000,
|
|
135
|
-
splits: new JBSplit[](0)
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
vm.prank(address(testHook));
|
|
139
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
140
|
-
|
|
141
|
-
// Seed 1 ether of pay credits with an earlier overpayment.
|
|
142
|
-
uint16[] memory noTiers = new uint16[](0);
|
|
143
|
-
JBAfterPayRecordedContext memory creditSeedContext = JBAfterPayRecordedContext({
|
|
144
|
-
payer: beneficiary,
|
|
145
|
-
projectId: projectId,
|
|
146
|
-
rulesetId: 0,
|
|
147
|
-
amount: JBTokenAmount({
|
|
148
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
149
|
-
value: 1 ether,
|
|
150
|
-
decimals: 18,
|
|
151
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
152
|
-
}),
|
|
153
|
-
forwardedAmount: JBTokenAmount({
|
|
154
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
155
|
-
value: 0,
|
|
156
|
-
decimals: 18,
|
|
157
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
158
|
-
}),
|
|
159
|
-
weight: 10e18,
|
|
160
|
-
newlyIssuedTokenCount: 0,
|
|
161
|
-
beneficiary: beneficiary,
|
|
162
|
-
hookMetadata: "",
|
|
163
|
-
payerMetadata: _buildPayMetadata(address(testHook), true, noTiers)
|
|
164
|
-
});
|
|
165
|
-
vm.prank(mockTerminalAddress);
|
|
166
|
-
testHook.afterPayRecordedWith(creditSeedContext);
|
|
167
|
-
assertEq(testHook.payCreditsOf(beneficiary), 1 ether, "setup: pay credits should be seeded");
|
|
168
|
-
|
|
169
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
170
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
171
|
-
|
|
172
|
-
// beforePay caps the forwarded amount to the actual payment...
|
|
173
|
-
(, JBPayHookSpecification[] memory specs) =
|
|
174
|
-
testHook.beforePayRecordedWith(_beforePayContext(address(testHook), 1, mintIds));
|
|
175
|
-
assertEq(specs[0].amount, 1, "forwarded amount should be capped to actual payment");
|
|
176
|
-
|
|
177
|
-
// ...and proportionally scales the encoded per-tier split amounts to match the capped total.
|
|
178
|
-
(,, bytes memory splitData) = abi.decode(specs[0].metadata, (address, address, bytes));
|
|
179
|
-
(, uint256[] memory encodedAmounts) = abi.decode(splitData, (uint16[], uint256[]));
|
|
180
|
-
assertEq(encodedAmounts.length, 1, "expected one encoded split amount");
|
|
181
|
-
assertEq(encodedAmounts[0], 1, "hook metadata should be scaled down to match forwarded amount");
|
|
182
|
-
|
|
183
|
-
// Route the split to a beneficiary and make project-balance fallback unavailable.
|
|
184
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
185
|
-
splits[0] = JBSplit({
|
|
186
|
-
percent: 1_000_000_000,
|
|
187
|
-
projectId: 0,
|
|
188
|
-
beneficiary: payable(splitBeneficiary),
|
|
189
|
-
preferAddToBalance: false,
|
|
190
|
-
lockedUntil: 0,
|
|
191
|
-
hook: IJBSplitHook(address(0))
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
195
|
-
vm.mockCall(
|
|
196
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
197
|
-
);
|
|
198
|
-
vm.mockCall(
|
|
199
|
-
mockJBDirectory,
|
|
200
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
|
|
201
|
-
abi.encode(address(0))
|
|
202
|
-
);
|
|
203
|
-
|
|
204
|
-
uint256 splitBeneficiaryBalanceBefore = splitBeneficiary.balance;
|
|
205
|
-
|
|
206
|
-
// The mint succeeds because pay credits cover the price. With the fix, the split IS honored.
|
|
207
|
-
JBAfterPayRecordedContext memory creditMintContext =
|
|
208
|
-
_afterPayContext(address(testHook), 1, 1, specs[0].metadata, mintIds);
|
|
209
|
-
vm.deal(mockTerminalAddress, 1);
|
|
210
|
-
vm.prank(mockTerminalAddress);
|
|
211
|
-
testHook.afterPayRecordedWith{value: 1}(creditMintContext);
|
|
212
|
-
|
|
213
|
-
assertEq(testHook.balanceOf(beneficiary), 1, "beneficiary should still receive the NFT");
|
|
214
|
-
assertEq(testHook.payCreditsOf(beneficiary), 1, "only 1 wei of credits should remain");
|
|
215
|
-
// After fix: split beneficiary receives the forwarded ETH, nothing trapped.
|
|
216
|
-
assertEq(splitBeneficiary.balance - splitBeneficiaryBalanceBefore, 1, "split beneficiary should receive 1 wei");
|
|
217
|
-
assertEq(address(testHook).balance, 0, "no ETH should be trapped in the hook");
|
|
218
|
-
}
|
|
219
|
-
}
|
|
@@ -1,143 +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
|
-
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
7
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
8
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
9
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
10
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
11
|
-
|
|
12
|
-
contract SplitFailureRedistribution is UnitTestSetup {
|
|
13
|
-
address internal alice = makeAddr("alice");
|
|
14
|
-
address internal bob = makeAddr("bob");
|
|
15
|
-
|
|
16
|
-
function test_failedEarlierSplit_doesNotOverpayLaterSplit() public {
|
|
17
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
18
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
19
|
-
|
|
20
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
21
|
-
tierConfigs[0].price = 1 ether;
|
|
22
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
23
|
-
tierConfigs[0].category = uint24(1);
|
|
24
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
25
|
-
tierConfigs[0].splitPercent = 1_000_000_000;
|
|
26
|
-
|
|
27
|
-
vm.prank(address(testHook));
|
|
28
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
29
|
-
|
|
30
|
-
mockAndExpect(
|
|
31
|
-
address(mockJBDirectory),
|
|
32
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
33
|
-
abi.encode(true)
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
RevertOnReceive revertingBeneficiary = new RevertOnReceive();
|
|
37
|
-
|
|
38
|
-
JBSplit[] memory splits = new JBSplit[](2);
|
|
39
|
-
splits[0] = JBSplit({
|
|
40
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2),
|
|
41
|
-
projectId: 0,
|
|
42
|
-
beneficiary: payable(address(revertingBeneficiary)),
|
|
43
|
-
preferAddToBalance: false,
|
|
44
|
-
lockedUntil: 0,
|
|
45
|
-
hook: IJBSplitHook(address(0))
|
|
46
|
-
});
|
|
47
|
-
splits[1] = JBSplit({
|
|
48
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2),
|
|
49
|
-
projectId: 0,
|
|
50
|
-
beneficiary: payable(bob),
|
|
51
|
-
preferAddToBalance: false,
|
|
52
|
-
lockedUntil: 0,
|
|
53
|
-
hook: IJBSplitHook(address(0))
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
57
|
-
mockAndExpect(
|
|
58
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
bytes memory payerMetadata = _buildPayMetadata(address(testHook), uint16(tierIds[0]));
|
|
62
|
-
bytes memory hookMetadata =
|
|
63
|
-
abi.encode(beneficiary, beneficiary, abi.encode(_singleTierId(uint16(tierIds[0])), _singleAmount(1 ether)));
|
|
64
|
-
|
|
65
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
66
|
-
payer: beneficiary,
|
|
67
|
-
projectId: projectId,
|
|
68
|
-
rulesetId: 0,
|
|
69
|
-
amount: JBTokenAmount({
|
|
70
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
71
|
-
value: 1 ether,
|
|
72
|
-
decimals: 18,
|
|
73
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
74
|
-
}),
|
|
75
|
-
forwardedAmount: JBTokenAmount({
|
|
76
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
77
|
-
value: 1 ether,
|
|
78
|
-
decimals: 18,
|
|
79
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
80
|
-
}),
|
|
81
|
-
weight: 10e18,
|
|
82
|
-
newlyIssuedTokenCount: 0,
|
|
83
|
-
beneficiary: beneficiary,
|
|
84
|
-
hookMetadata: hookMetadata,
|
|
85
|
-
payerMetadata: payerMetadata
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Mock primaryTerminalOf so the leftover (failed split amount) can be routed to the project balance.
|
|
89
|
-
address projectTerminal = makeAddr("projectTerminal");
|
|
90
|
-
vm.etch(projectTerminal, new bytes(0x69));
|
|
91
|
-
mockAndExpect(
|
|
92
|
-
address(mockJBDirectory),
|
|
93
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
|
|
94
|
-
abi.encode(projectTerminal)
|
|
95
|
-
);
|
|
96
|
-
// Mock addToBalanceOf on the terminal so the leftover deposit succeeds.
|
|
97
|
-
vm.mockCall(
|
|
98
|
-
projectTerminal,
|
|
99
|
-
abi.encodeWithSelector(
|
|
100
|
-
IJBTerminal.addToBalanceOf.selector, projectId, JBConstants.NATIVE_TOKEN, 0.5 ether, false, "", ""
|
|
101
|
-
),
|
|
102
|
-
""
|
|
103
|
-
);
|
|
104
|
-
|
|
105
|
-
uint256 bobBalanceBefore = bob.balance;
|
|
106
|
-
|
|
107
|
-
vm.deal(mockTerminalAddress, 1 ether);
|
|
108
|
-
vm.prank(mockTerminalAddress);
|
|
109
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
110
|
-
|
|
111
|
-
// With the fix, Bob only receives his fair 50% share. The failed split's 0.5 ether
|
|
112
|
-
// is routed to the project's balance via addToBalanceOf on the primary terminal.
|
|
113
|
-
assertEq(
|
|
114
|
-
bob.balance - bobBalanceBefore,
|
|
115
|
-
0.5 ether,
|
|
116
|
-
"later split should only receive its own allocation, not the failed split's share"
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function _buildPayMetadata(address hookAddress, uint16 tierId) internal view returns (bytes memory) {
|
|
121
|
-
bytes[] memory data = new bytes[](1);
|
|
122
|
-
data[0] = abi.encode(false, _singleTierId(tierId));
|
|
123
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
124
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
125
|
-
return metadataHelper.createMetadata(ids, data);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function _singleTierId(uint16 tierId) internal pure returns (uint16[] memory tierIds) {
|
|
129
|
-
tierIds = new uint16[](1);
|
|
130
|
-
tierIds[0] = tierId;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function _singleAmount(uint256 amount) internal pure returns (uint256[] memory amounts) {
|
|
134
|
-
amounts = new uint256[](1);
|
|
135
|
-
amounts[0] = amount;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
contract RevertOnReceive {
|
|
140
|
-
receive() external payable {
|
|
141
|
-
revert("NO_ETH");
|
|
142
|
-
}
|
|
143
|
-
}
|