@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,757 +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 {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
8
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
9
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
10
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
11
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
12
|
-
import {JB721TiersHookFlags} from "../../src/structs/JB721TiersHookFlags.sol";
|
|
13
|
-
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
14
|
-
|
|
15
|
-
contract MockERC20 is ERC20 {
|
|
16
|
-
constructor() ERC20("Mock Token", "MOCK") {}
|
|
17
|
-
|
|
18
|
-
function mint(address to, uint256 amount) external {
|
|
19
|
-
_mint(to, amount);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
contract Test_TierSplitRouting is UnitTestSetup {
|
|
24
|
-
using stdStorage for StdStorage;
|
|
25
|
-
|
|
26
|
-
address alice = makeAddr("alice");
|
|
27
|
-
address bob = makeAddr("bob");
|
|
28
|
-
|
|
29
|
-
function setUp() public override {
|
|
30
|
-
super.setUp();
|
|
31
|
-
vm.etch(mockJBSplits, new bytes(0x69));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Helper: build a tier config with splits.
|
|
35
|
-
function _tierConfigWithSplit(
|
|
36
|
-
uint104 price,
|
|
37
|
-
uint32 splitPercent
|
|
38
|
-
)
|
|
39
|
-
internal
|
|
40
|
-
pure
|
|
41
|
-
returns (JB721TierConfig memory config)
|
|
42
|
-
{
|
|
43
|
-
config.price = price;
|
|
44
|
-
config.initialSupply = uint32(100);
|
|
45
|
-
config.category = uint24(1);
|
|
46
|
-
config.encodedIPFSUri = bytes32(uint256(0x1234));
|
|
47
|
-
config.splitPercent = splitPercent;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Helper: build payer metadata for tier IDs.
|
|
51
|
-
function _buildPayerMetadata(
|
|
52
|
-
address hookAddress,
|
|
53
|
-
uint16[] memory tierIdsToMint
|
|
54
|
-
)
|
|
55
|
-
internal
|
|
56
|
-
view
|
|
57
|
-
returns (bytes memory)
|
|
58
|
-
{
|
|
59
|
-
bytes[] memory data = new bytes[](1);
|
|
60
|
-
data[0] = abi.encode(false, tierIdsToMint);
|
|
61
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
62
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
63
|
-
return metadataHelper.createMetadata(ids, data);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ──────────────────────────────────────────────
|
|
67
|
-
// Test: beforePayRecordedWith calculates split amount
|
|
68
|
-
// ──────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
function test_beforePayRecorded_calculatesSplitAmount() public {
|
|
71
|
-
// Create hook with a default tier (no splits).
|
|
72
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
73
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
74
|
-
|
|
75
|
-
// Add a tier with 50% split directly to the hook's store.
|
|
76
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
77
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000); // 50%
|
|
78
|
-
vm.prank(address(testHook));
|
|
79
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
80
|
-
|
|
81
|
-
// Build payer metadata requesting that tier.
|
|
82
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
83
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
84
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
85
|
-
|
|
86
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
87
|
-
terminal: mockTerminalAddress,
|
|
88
|
-
payer: beneficiary,
|
|
89
|
-
amount: JBTokenAmount({
|
|
90
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
91
|
-
value: 1 ether,
|
|
92
|
-
decimals: 18,
|
|
93
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
94
|
-
}),
|
|
95
|
-
projectId: projectId,
|
|
96
|
-
rulesetId: 0,
|
|
97
|
-
beneficiary: beneficiary,
|
|
98
|
-
weight: 10e18,
|
|
99
|
-
reservedPercent: 5000,
|
|
100
|
-
metadata: payerMetadata
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
(uint256 weight, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
104
|
-
|
|
105
|
-
// Weight adjusted for 50% split: 10e18 * 0.5 = 5e18.
|
|
106
|
-
assertEq(weight, 5e18);
|
|
107
|
-
// Hook spec should forward 50% of 1 ETH = 0.5 ETH.
|
|
108
|
-
assertEq(specs.length, 1);
|
|
109
|
-
assertEq(specs[0].amount, 0.5 ether);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ──────────────────────────────────────────────
|
|
113
|
-
// Test: no splitPercent means no forwarded amount
|
|
114
|
-
// ──────────────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
function test_beforePayRecorded_noSplitPercent_noForwardedAmount() public {
|
|
117
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
118
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
119
|
-
|
|
120
|
-
// Add a tier with 0% split.
|
|
121
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
122
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 0);
|
|
123
|
-
vm.prank(address(testHook));
|
|
124
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
125
|
-
|
|
126
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
127
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
128
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
129
|
-
|
|
130
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
131
|
-
terminal: mockTerminalAddress,
|
|
132
|
-
payer: beneficiary,
|
|
133
|
-
amount: JBTokenAmount({
|
|
134
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
135
|
-
value: 1 ether,
|
|
136
|
-
decimals: 18,
|
|
137
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
138
|
-
}),
|
|
139
|
-
projectId: projectId,
|
|
140
|
-
rulesetId: 0,
|
|
141
|
-
beneficiary: beneficiary,
|
|
142
|
-
weight: 10e18,
|
|
143
|
-
reservedPercent: 5000,
|
|
144
|
-
metadata: payerMetadata
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
148
|
-
|
|
149
|
-
// No split amount forwarded.
|
|
150
|
-
assertEq(specs[0].amount, 0);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ──────────────────────────────────────────────
|
|
154
|
-
// Test: multiple tiers with different split percents
|
|
155
|
-
// ──────────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
function test_beforePayRecorded_multipleTiersDifferentSplitPercents() public {
|
|
158
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
159
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
160
|
-
|
|
161
|
-
// Tier 1: 1 ETH, 30% split. Tier 2: 2 ETH, 100% split.
|
|
162
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](2);
|
|
163
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 300_000_000);
|
|
164
|
-
tierConfigs[0].category = 1;
|
|
165
|
-
tierConfigs[1] = _tierConfigWithSplit(2 ether, 1_000_000_000);
|
|
166
|
-
tierConfigs[1].category = 2;
|
|
167
|
-
vm.prank(address(testHook));
|
|
168
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
169
|
-
|
|
170
|
-
uint16[] memory mintIds = new uint16[](2);
|
|
171
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
172
|
-
mintIds[1] = uint16(tierIds[1]);
|
|
173
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
174
|
-
|
|
175
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
176
|
-
terminal: mockTerminalAddress,
|
|
177
|
-
payer: beneficiary,
|
|
178
|
-
amount: JBTokenAmount({
|
|
179
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
180
|
-
value: 3 ether,
|
|
181
|
-
decimals: 18,
|
|
182
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
183
|
-
}),
|
|
184
|
-
projectId: projectId,
|
|
185
|
-
rulesetId: 0,
|
|
186
|
-
beneficiary: beneficiary,
|
|
187
|
-
weight: 10e18,
|
|
188
|
-
reservedPercent: 5000,
|
|
189
|
-
metadata: payerMetadata
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
193
|
-
|
|
194
|
-
// Total split = 1 ETH * 30% + 2 ETH * 100% = 0.3 + 2.0 = 2.3 ETH.
|
|
195
|
-
assertEq(specs[0].amount, 2.3 ether);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// ──────────────────────────────────────────────
|
|
199
|
-
// Test: weight adjusted for splits
|
|
200
|
-
// ──────────────────────────────────────────────
|
|
201
|
-
|
|
202
|
-
function test_beforePayRecorded_weightAdjustedForSplits() public {
|
|
203
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
204
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
205
|
-
|
|
206
|
-
// Add a tier with 30% split.
|
|
207
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
208
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 300_000_000); // 30%
|
|
209
|
-
vm.prank(address(testHook));
|
|
210
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
211
|
-
|
|
212
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
213
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
214
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
215
|
-
|
|
216
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
217
|
-
terminal: mockTerminalAddress,
|
|
218
|
-
payer: beneficiary,
|
|
219
|
-
amount: JBTokenAmount({
|
|
220
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
221
|
-
value: 1 ether,
|
|
222
|
-
decimals: 18,
|
|
223
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
224
|
-
}),
|
|
225
|
-
projectId: projectId,
|
|
226
|
-
rulesetId: 0,
|
|
227
|
-
beneficiary: beneficiary,
|
|
228
|
-
weight: 10e18,
|
|
229
|
-
reservedPercent: 5000,
|
|
230
|
-
metadata: payerMetadata
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
(uint256 weight,) = testHook.beforePayRecordedWith(context);
|
|
234
|
-
|
|
235
|
-
// Weight adjusted for 30% split: 10e18 * 0.7 = 7e18.
|
|
236
|
-
assertEq(weight, 7e18);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function test_beforePayRecorded_noSplitPercent_weightUnchanged() public {
|
|
240
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
241
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
242
|
-
|
|
243
|
-
// Add a tier with 0% split.
|
|
244
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
245
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 0);
|
|
246
|
-
vm.prank(address(testHook));
|
|
247
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
248
|
-
|
|
249
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
250
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
251
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
252
|
-
|
|
253
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
254
|
-
terminal: mockTerminalAddress,
|
|
255
|
-
payer: beneficiary,
|
|
256
|
-
amount: JBTokenAmount({
|
|
257
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
258
|
-
value: 1 ether,
|
|
259
|
-
decimals: 18,
|
|
260
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
261
|
-
}),
|
|
262
|
-
projectId: projectId,
|
|
263
|
-
rulesetId: 0,
|
|
264
|
-
beneficiary: beneficiary,
|
|
265
|
-
weight: 10e18,
|
|
266
|
-
reservedPercent: 5000,
|
|
267
|
-
metadata: payerMetadata
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
(uint256 weight,) = testHook.beforePayRecordedWith(context);
|
|
271
|
-
|
|
272
|
-
// No split = weight unchanged.
|
|
273
|
-
assertEq(weight, 10e18);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function test_beforePayRecorded_fullSplitPercent_weightZero() public {
|
|
277
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
278
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
279
|
-
|
|
280
|
-
// Add a tier with 100% split, priced at full payment amount.
|
|
281
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
282
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 1_000_000_000); // 100%
|
|
283
|
-
vm.prank(address(testHook));
|
|
284
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
285
|
-
|
|
286
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
287
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
288
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
289
|
-
|
|
290
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
291
|
-
terminal: mockTerminalAddress,
|
|
292
|
-
payer: beneficiary,
|
|
293
|
-
amount: JBTokenAmount({
|
|
294
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
295
|
-
value: 1 ether,
|
|
296
|
-
decimals: 18,
|
|
297
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
298
|
-
}),
|
|
299
|
-
projectId: projectId,
|
|
300
|
-
rulesetId: 0,
|
|
301
|
-
beneficiary: beneficiary,
|
|
302
|
-
weight: 10e18,
|
|
303
|
-
reservedPercent: 5000,
|
|
304
|
-
metadata: payerMetadata
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
(uint256 weight,) = testHook.beforePayRecordedWith(context);
|
|
308
|
-
|
|
309
|
-
// 100% split of full amount = weight 0.
|
|
310
|
-
assertEq(weight, 0);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ──────────────────────────────────────────────
|
|
314
|
-
// Test: afterPayRecordedWith distributes to split beneficiary
|
|
315
|
-
// ──────────────────────────────────────────────
|
|
316
|
-
|
|
317
|
-
function test_afterPayRecorded_distributesToBeneficiary() public {
|
|
318
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
319
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
320
|
-
|
|
321
|
-
// Add a tier with 50% split.
|
|
322
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
323
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000);
|
|
324
|
-
vm.prank(address(testHook));
|
|
325
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
326
|
-
|
|
327
|
-
// Mock directory checks.
|
|
328
|
-
mockAndExpect(
|
|
329
|
-
address(mockJBDirectory),
|
|
330
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
331
|
-
abi.encode(true)
|
|
332
|
-
);
|
|
333
|
-
|
|
334
|
-
// Mock splits: alice gets 100%.
|
|
335
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
336
|
-
splits[0] = JBSplit({
|
|
337
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
338
|
-
projectId: 0,
|
|
339
|
-
beneficiary: payable(alice),
|
|
340
|
-
preferAddToBalance: false,
|
|
341
|
-
lockedUntil: 0,
|
|
342
|
-
hook: IJBSplitHook(address(0))
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
346
|
-
mockAndExpect(
|
|
347
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
348
|
-
);
|
|
349
|
-
|
|
350
|
-
// Build payer metadata.
|
|
351
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
352
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
353
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
354
|
-
|
|
355
|
-
// Build hook metadata (per-tier split breakdown from beforePayRecordedWith).
|
|
356
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
357
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
358
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
359
|
-
splitAmounts[0] = 0.5 ether;
|
|
360
|
-
|
|
361
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
362
|
-
payer: beneficiary,
|
|
363
|
-
projectId: projectId,
|
|
364
|
-
rulesetId: 0,
|
|
365
|
-
amount: JBTokenAmount({
|
|
366
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
367
|
-
value: 1 ether,
|
|
368
|
-
decimals: 18,
|
|
369
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
370
|
-
}),
|
|
371
|
-
forwardedAmount: JBTokenAmount({
|
|
372
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
373
|
-
value: 0.5 ether,
|
|
374
|
-
decimals: 18,
|
|
375
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
376
|
-
}),
|
|
377
|
-
weight: 10e18,
|
|
378
|
-
newlyIssuedTokenCount: 0,
|
|
379
|
-
beneficiary: beneficiary,
|
|
380
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
381
|
-
payerMetadata: payerMetadata
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
uint256 aliceBalanceBefore = alice.balance;
|
|
385
|
-
|
|
386
|
-
vm.deal(mockTerminalAddress, 1 ether);
|
|
387
|
-
vm.prank(mockTerminalAddress);
|
|
388
|
-
testHook.afterPayRecordedWith{value: 0.5 ether}(payContext);
|
|
389
|
-
|
|
390
|
-
// Alice should have received 0.5 ETH.
|
|
391
|
-
assertEq(alice.balance - aliceBalanceBefore, 0.5 ether);
|
|
392
|
-
// NFT should have been minted.
|
|
393
|
-
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// ──────────────────────────────────────────────
|
|
397
|
-
// Test: issueTokensForSplits flag gives full weight with partial splits
|
|
398
|
-
// ──────────────────────────────────────────────
|
|
399
|
-
|
|
400
|
-
function test_beforePayRecorded_issueTokensForSplits_fullWeight() public {
|
|
401
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
402
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
403
|
-
|
|
404
|
-
// Set the issueTokensForSplits flag.
|
|
405
|
-
vm.prank(address(testHook));
|
|
406
|
-
hookStore.recordFlags(
|
|
407
|
-
JB721TiersHookFlags({
|
|
408
|
-
noNewTiersWithReserves: false,
|
|
409
|
-
noNewTiersWithVotes: false,
|
|
410
|
-
noNewTiersWithOwnerMinting: false,
|
|
411
|
-
preventOverspending: false,
|
|
412
|
-
issueTokensForSplits: true
|
|
413
|
-
})
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
// Add a tier with 50% split.
|
|
417
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
418
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000); // 50%
|
|
419
|
-
vm.prank(address(testHook));
|
|
420
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
421
|
-
|
|
422
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
423
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
424
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
425
|
-
|
|
426
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
427
|
-
terminal: mockTerminalAddress,
|
|
428
|
-
payer: beneficiary,
|
|
429
|
-
amount: JBTokenAmount({
|
|
430
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
431
|
-
value: 1 ether,
|
|
432
|
-
decimals: 18,
|
|
433
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
434
|
-
}),
|
|
435
|
-
projectId: projectId,
|
|
436
|
-
rulesetId: 0,
|
|
437
|
-
beneficiary: beneficiary,
|
|
438
|
-
weight: 10e18,
|
|
439
|
-
reservedPercent: 5000,
|
|
440
|
-
metadata: payerMetadata
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
(uint256 weight,) = testHook.beforePayRecordedWith(context);
|
|
444
|
-
|
|
445
|
-
// Flag set — weight should be full despite 50% split.
|
|
446
|
-
assertEq(weight, 10e18);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// ──────────────────────────────────────────────
|
|
450
|
-
// Test: issueTokensForSplits flag gives full weight even when splits consume entire payment
|
|
451
|
-
// ──────────────────────────────────────────────
|
|
452
|
-
|
|
453
|
-
function test_beforePayRecorded_issueTokensForSplits_fullSplit_stillFullWeight() public {
|
|
454
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
455
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
456
|
-
|
|
457
|
-
// Set the issueTokensForSplits flag.
|
|
458
|
-
vm.prank(address(testHook));
|
|
459
|
-
hookStore.recordFlags(
|
|
460
|
-
JB721TiersHookFlags({
|
|
461
|
-
noNewTiersWithReserves: false,
|
|
462
|
-
noNewTiersWithVotes: false,
|
|
463
|
-
noNewTiersWithOwnerMinting: false,
|
|
464
|
-
preventOverspending: false,
|
|
465
|
-
issueTokensForSplits: true
|
|
466
|
-
})
|
|
467
|
-
);
|
|
468
|
-
|
|
469
|
-
// Add a tier with 100% split, priced at full payment amount.
|
|
470
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
471
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 1_000_000_000); // 100%
|
|
472
|
-
vm.prank(address(testHook));
|
|
473
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
474
|
-
|
|
475
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
476
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
477
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
478
|
-
|
|
479
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
480
|
-
terminal: mockTerminalAddress,
|
|
481
|
-
payer: beneficiary,
|
|
482
|
-
amount: JBTokenAmount({
|
|
483
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
484
|
-
value: 1 ether,
|
|
485
|
-
decimals: 18,
|
|
486
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
487
|
-
}),
|
|
488
|
-
projectId: projectId,
|
|
489
|
-
rulesetId: 0,
|
|
490
|
-
beneficiary: beneficiary,
|
|
491
|
-
weight: 10e18,
|
|
492
|
-
reservedPercent: 5000,
|
|
493
|
-
metadata: payerMetadata
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
(uint256 weight,) = testHook.beforePayRecordedWith(context);
|
|
497
|
-
|
|
498
|
-
// Flag set — weight should be full despite 100% split consuming entire payment.
|
|
499
|
-
assertEq(weight, 10e18);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// ──────────────────────────────────────────────
|
|
503
|
-
// ERC20 Tests: afterPayRecordedWith with ERC20 tokens
|
|
504
|
-
// ──────────────────────────────────────────────
|
|
505
|
-
|
|
506
|
-
/// @notice Helper: set up an ERC20 tier split test. Returns (hook, tierIds, mockToken).
|
|
507
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
508
|
-
function _setupERC20TierSplit()
|
|
509
|
-
internal
|
|
510
|
-
returns (JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token)
|
|
511
|
-
{
|
|
512
|
-
// Deploy mock ERC20.
|
|
513
|
-
token = new MockERC20();
|
|
514
|
-
|
|
515
|
-
// Initialize hook with ERC20 currency (0 default tiers).
|
|
516
|
-
testHook = _initHookDefaultTiers(0, false, uint32(uint160(address(token))), 18);
|
|
517
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
518
|
-
|
|
519
|
-
// Add a tier with 50% split, priced at 100 tokens.
|
|
520
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
521
|
-
tierConfigs[0] = _tierConfigWithSplit(100, 500_000_000); // 50%
|
|
522
|
-
vm.prank(address(testHook));
|
|
523
|
-
tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
524
|
-
|
|
525
|
-
// Mock directory checks.
|
|
526
|
-
mockAndExpect(
|
|
527
|
-
address(mockJBDirectory),
|
|
528
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
529
|
-
abi.encode(true)
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
/// @notice Helper: build afterPayRecordedWith context for ERC20 payments.
|
|
534
|
-
// forge-lint: disable-next-line(mixed-case-function)
|
|
535
|
-
function _buildERC20PayContext(
|
|
536
|
-
JB721TiersHook testHook,
|
|
537
|
-
uint256[] memory tierIds,
|
|
538
|
-
address token,
|
|
539
|
-
uint256 payAmount,
|
|
540
|
-
uint256 forwardedAmount
|
|
541
|
-
)
|
|
542
|
-
internal
|
|
543
|
-
view
|
|
544
|
-
returns (JBAfterPayRecordedContext memory)
|
|
545
|
-
{
|
|
546
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
547
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
548
|
-
// Use METADATA_ID_TARGET (the original hook address) for metadata resolution.
|
|
549
|
-
bytes memory payerMetadata = _buildPayerMetadata(testHook.METADATA_ID_TARGET(), mintIds);
|
|
550
|
-
|
|
551
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
552
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
553
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
554
|
-
splitAmounts[0] = forwardedAmount;
|
|
555
|
-
|
|
556
|
-
return JBAfterPayRecordedContext({
|
|
557
|
-
payer: beneficiary,
|
|
558
|
-
projectId: projectId,
|
|
559
|
-
rulesetId: 0,
|
|
560
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
561
|
-
amount: JBTokenAmount({token: token, value: payAmount, decimals: 18, currency: uint32(uint160(token))}),
|
|
562
|
-
forwardedAmount: JBTokenAmount({
|
|
563
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
564
|
-
token: token,
|
|
565
|
-
value: forwardedAmount,
|
|
566
|
-
decimals: 18,
|
|
567
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
568
|
-
currency: uint32(uint160(token))
|
|
569
|
-
}),
|
|
570
|
-
weight: 10e18,
|
|
571
|
-
newlyIssuedTokenCount: 0,
|
|
572
|
-
beneficiary: beneficiary,
|
|
573
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
574
|
-
payerMetadata: payerMetadata
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function test_afterPayRecorded_erc20_distributesToBeneficiary() public {
|
|
579
|
-
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
580
|
-
|
|
581
|
-
// Mock splits: alice gets 100%.
|
|
582
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
583
|
-
splits[0] = JBSplit({
|
|
584
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
585
|
-
projectId: 0,
|
|
586
|
-
beneficiary: payable(alice),
|
|
587
|
-
preferAddToBalance: false,
|
|
588
|
-
lockedUntil: 0,
|
|
589
|
-
hook: IJBSplitHook(address(0))
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
593
|
-
mockAndExpect(
|
|
594
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
// Give terminal the ERC20 tokens and approve the hook.
|
|
598
|
-
token.mint(mockTerminalAddress, 100);
|
|
599
|
-
vm.prank(mockTerminalAddress);
|
|
600
|
-
token.approve(address(testHook), 50);
|
|
601
|
-
|
|
602
|
-
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
603
|
-
|
|
604
|
-
vm.prank(mockTerminalAddress);
|
|
605
|
-
testHook.afterPayRecordedWith(payContext);
|
|
606
|
-
|
|
607
|
-
// Alice should have received 50 tokens.
|
|
608
|
-
assertEq(token.balanceOf(alice), 50);
|
|
609
|
-
// NFT should have been minted.
|
|
610
|
-
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
function test_afterPayRecorded_erc20_splitToProject_addToBalance() public {
|
|
614
|
-
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
615
|
-
|
|
616
|
-
// Target project for split.
|
|
617
|
-
uint256 targetProjectId = 99;
|
|
618
|
-
address targetTerminal = makeAddr("targetTerminal");
|
|
619
|
-
vm.etch(targetTerminal, new bytes(0x69));
|
|
620
|
-
|
|
621
|
-
// Mock splits: 100% to target project with preferAddToBalance.
|
|
622
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
623
|
-
splits[0] = JBSplit({
|
|
624
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
625
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
626
|
-
projectId: uint56(targetProjectId),
|
|
627
|
-
beneficiary: payable(address(0)),
|
|
628
|
-
preferAddToBalance: true,
|
|
629
|
-
lockedUntil: 0,
|
|
630
|
-
hook: IJBSplitHook(address(0))
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
634
|
-
mockAndExpect(
|
|
635
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
636
|
-
);
|
|
637
|
-
|
|
638
|
-
// Mock directory: target project's primary terminal.
|
|
639
|
-
mockAndExpect(
|
|
640
|
-
address(mockJBDirectory),
|
|
641
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, targetProjectId, address(token)),
|
|
642
|
-
abi.encode(targetTerminal)
|
|
643
|
-
);
|
|
644
|
-
|
|
645
|
-
// Mock the addToBalanceOf call on the target terminal.
|
|
646
|
-
vm.mockCall(targetTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
|
|
647
|
-
|
|
648
|
-
// Give terminal the ERC20 tokens and approve the hook.
|
|
649
|
-
token.mint(mockTerminalAddress, 100);
|
|
650
|
-
vm.prank(mockTerminalAddress);
|
|
651
|
-
token.approve(address(testHook), 50);
|
|
652
|
-
|
|
653
|
-
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
654
|
-
|
|
655
|
-
vm.prank(mockTerminalAddress);
|
|
656
|
-
testHook.afterPayRecordedWith(payContext);
|
|
657
|
-
|
|
658
|
-
// Hook approved the target terminal (library calls forceApprove before addToBalanceOf).
|
|
659
|
-
// Mock terminal doesn't pull, so hook still holds the tokens. Verify approval was set.
|
|
660
|
-
assertGe(token.allowance(address(testHook), targetTerminal), 50);
|
|
661
|
-
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
function test_afterPayRecorded_erc20_splitToProject_pay() public {
|
|
665
|
-
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
666
|
-
|
|
667
|
-
uint256 targetProjectId = 99;
|
|
668
|
-
address targetTerminal = makeAddr("targetTerminal");
|
|
669
|
-
vm.etch(targetTerminal, new bytes(0x69));
|
|
670
|
-
|
|
671
|
-
// Mock splits: 100% to target project with preferAddToBalance = false (pay).
|
|
672
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
673
|
-
splits[0] = JBSplit({
|
|
674
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
675
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
676
|
-
projectId: uint56(targetProjectId),
|
|
677
|
-
beneficiary: payable(alice),
|
|
678
|
-
preferAddToBalance: false,
|
|
679
|
-
lockedUntil: 0,
|
|
680
|
-
hook: IJBSplitHook(address(0))
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
684
|
-
mockAndExpect(
|
|
685
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
686
|
-
);
|
|
687
|
-
|
|
688
|
-
// Mock directory: target project's primary terminal.
|
|
689
|
-
mockAndExpect(
|
|
690
|
-
address(mockJBDirectory),
|
|
691
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, targetProjectId, address(token)),
|
|
692
|
-
abi.encode(targetTerminal)
|
|
693
|
-
);
|
|
694
|
-
|
|
695
|
-
// Mock the pay call on the target terminal.
|
|
696
|
-
vm.mockCall(targetTerminal, abi.encodeWithSelector(IJBTerminal.pay.selector), abi.encode(0));
|
|
697
|
-
|
|
698
|
-
// Give terminal the ERC20 tokens and approve the hook.
|
|
699
|
-
token.mint(mockTerminalAddress, 100);
|
|
700
|
-
vm.prank(mockTerminalAddress);
|
|
701
|
-
token.approve(address(testHook), 50);
|
|
702
|
-
|
|
703
|
-
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
704
|
-
|
|
705
|
-
vm.prank(mockTerminalAddress);
|
|
706
|
-
testHook.afterPayRecordedWith(payContext);
|
|
707
|
-
|
|
708
|
-
// Hook approved the target terminal (library calls forceApprove before pay).
|
|
709
|
-
assertGe(token.allowance(address(testHook), targetTerminal), 50);
|
|
710
|
-
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function test_afterPayRecorded_erc20_noBeneficiary_routesToProjectBalance() public {
|
|
714
|
-
(JB721TiersHook testHook, uint256[] memory tierIds, MockERC20 token) = _setupERC20TierSplit();
|
|
715
|
-
|
|
716
|
-
// Mock splits: no beneficiary and no projectId — leftover goes to project balance.
|
|
717
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
718
|
-
splits[0] = JBSplit({
|
|
719
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
720
|
-
projectId: 0,
|
|
721
|
-
beneficiary: payable(address(0)),
|
|
722
|
-
preferAddToBalance: false,
|
|
723
|
-
lockedUntil: 0,
|
|
724
|
-
hook: IJBSplitHook(address(0))
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
728
|
-
mockAndExpect(
|
|
729
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
730
|
-
);
|
|
731
|
-
|
|
732
|
-
// Mock the project's primary terminal for the leftover addToBalance.
|
|
733
|
-
address projectTerminal = makeAddr("projectTerminal");
|
|
734
|
-
vm.etch(projectTerminal, new bytes(0x69));
|
|
735
|
-
mockAndExpect(
|
|
736
|
-
address(mockJBDirectory),
|
|
737
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, address(token)),
|
|
738
|
-
abi.encode(projectTerminal)
|
|
739
|
-
);
|
|
740
|
-
|
|
741
|
-
vm.mockCall(projectTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
|
|
742
|
-
|
|
743
|
-
// Give terminal the ERC20 tokens and approve the hook.
|
|
744
|
-
token.mint(mockTerminalAddress, 100);
|
|
745
|
-
vm.prank(mockTerminalAddress);
|
|
746
|
-
token.approve(address(testHook), 50);
|
|
747
|
-
|
|
748
|
-
JBAfterPayRecordedContext memory payContext = _buildERC20PayContext(testHook, tierIds, address(token), 100, 50);
|
|
749
|
-
|
|
750
|
-
vm.prank(mockTerminalAddress);
|
|
751
|
-
testHook.afterPayRecordedWith(payContext);
|
|
752
|
-
|
|
753
|
-
// Hook approved the project terminal (library calls forceApprove before addToBalanceOf).
|
|
754
|
-
assertGe(token.allowance(address(testHook), projectTerminal), 50);
|
|
755
|
-
assertEq(testHook.balanceOf(beneficiary), 1);
|
|
756
|
-
}
|
|
757
|
-
}
|