@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,604 +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 {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
9
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
10
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
11
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
12
|
-
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
13
|
-
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
14
|
-
|
|
15
|
-
/// @notice A mock split hook that records calls and accepts ETH/ERC20.
|
|
16
|
-
contract MockSplitHook is IJBSplitHook {
|
|
17
|
-
address public lastToken;
|
|
18
|
-
uint256 public lastAmount;
|
|
19
|
-
uint256 public lastDecimals;
|
|
20
|
-
uint256 public lastProjectId;
|
|
21
|
-
uint256 public lastGroupId;
|
|
22
|
-
JBSplit public lastSplit;
|
|
23
|
-
uint256 public callCount;
|
|
24
|
-
|
|
25
|
-
function processSplitWith(JBSplitHookContext calldata context) external payable override {
|
|
26
|
-
lastToken = context.token;
|
|
27
|
-
lastAmount = context.amount;
|
|
28
|
-
lastDecimals = context.decimals;
|
|
29
|
-
lastProjectId = context.projectId;
|
|
30
|
-
lastGroupId = context.groupId;
|
|
31
|
-
lastSplit = context.split;
|
|
32
|
-
callCount++;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
|
|
36
|
-
return interfaceId == type(IJBSplitHook).interfaceId || interfaceId == type(IERC165).interfaceId;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// @notice A mock ERC20 with configurable decimals (for USDC-like 6-decimal tokens).
|
|
41
|
-
contract MockERC20WithDecimals is ERC20 {
|
|
42
|
-
uint8 internal _decimals;
|
|
43
|
-
|
|
44
|
-
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
|
|
45
|
-
_decimals = decimals_;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function decimals() public view override returns (uint8) {
|
|
49
|
-
return _decimals;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function mint(address to, uint256 amount) external {
|
|
53
|
-
_mint(to, amount);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/// @notice Tests for split hook distribution in JB721TiersHookLib, covering ETH (18 decimals) and USDC (6 decimals).
|
|
58
|
-
contract Test_SplitHookDistribution is UnitTestSetup {
|
|
59
|
-
using stdStorage for StdStorage;
|
|
60
|
-
|
|
61
|
-
address alice = makeAddr("alice");
|
|
62
|
-
address bob = makeAddr("bob");
|
|
63
|
-
MockSplitHook splitHook;
|
|
64
|
-
|
|
65
|
-
function setUp() public override {
|
|
66
|
-
super.setUp();
|
|
67
|
-
vm.etch(mockJBSplits, new bytes(0x69));
|
|
68
|
-
splitHook = new MockSplitHook();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ───────────────────────────────────────────────────
|
|
72
|
-
// Helpers
|
|
73
|
-
// ───────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
function _tierConfigWithSplit(
|
|
76
|
-
uint104 price,
|
|
77
|
-
uint32 splitPercent
|
|
78
|
-
)
|
|
79
|
-
internal
|
|
80
|
-
pure
|
|
81
|
-
returns (JB721TierConfig memory config)
|
|
82
|
-
{
|
|
83
|
-
config.price = price;
|
|
84
|
-
config.initialSupply = uint32(100);
|
|
85
|
-
config.category = uint24(1);
|
|
86
|
-
config.encodedIPFSUri = bytes32(uint256(0x1234));
|
|
87
|
-
config.splitPercent = splitPercent;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function _buildPayerMetadata(
|
|
91
|
-
address hookAddress,
|
|
92
|
-
uint16[] memory tierIdsToMint
|
|
93
|
-
)
|
|
94
|
-
internal
|
|
95
|
-
view
|
|
96
|
-
returns (bytes memory)
|
|
97
|
-
{
|
|
98
|
-
bytes[] memory data = new bytes[](1);
|
|
99
|
-
data[0] = abi.encode(false, tierIdsToMint);
|
|
100
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
101
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
102
|
-
return metadataHelper.createMetadata(ids, data);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ───────────────────────────────────────────────────
|
|
106
|
-
// ETH Tests (18 decimals)
|
|
107
|
-
// ───────────────────────────────────────────────────
|
|
108
|
-
|
|
109
|
-
/// @notice Split hook receives ETH with correct context (decimals=18).
|
|
110
|
-
function test_splitHook_eth_receivesContextWithCorrectDecimals() public {
|
|
111
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
112
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
113
|
-
|
|
114
|
-
// Add tier with 50% split.
|
|
115
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
116
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000);
|
|
117
|
-
vm.prank(address(testHook));
|
|
118
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
119
|
-
|
|
120
|
-
// Mock directory.
|
|
121
|
-
mockAndExpect(
|
|
122
|
-
address(mockJBDirectory),
|
|
123
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
124
|
-
abi.encode(true)
|
|
125
|
-
);
|
|
126
|
-
|
|
127
|
-
// Splits: 100% to split hook.
|
|
128
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
129
|
-
splits[0] = JBSplit({
|
|
130
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
131
|
-
projectId: 0,
|
|
132
|
-
beneficiary: payable(address(0)),
|
|
133
|
-
preferAddToBalance: false,
|
|
134
|
-
lockedUntil: 0,
|
|
135
|
-
hook: IJBSplitHook(address(splitHook))
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
139
|
-
mockAndExpect(
|
|
140
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Build metadata.
|
|
144
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
145
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
146
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
147
|
-
|
|
148
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
149
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
150
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
151
|
-
splitAmounts[0] = 0.5 ether;
|
|
152
|
-
|
|
153
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
154
|
-
payer: beneficiary,
|
|
155
|
-
projectId: projectId,
|
|
156
|
-
rulesetId: 0,
|
|
157
|
-
amount: JBTokenAmount({
|
|
158
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
159
|
-
value: 1 ether,
|
|
160
|
-
decimals: 18,
|
|
161
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
162
|
-
}),
|
|
163
|
-
forwardedAmount: JBTokenAmount({
|
|
164
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
165
|
-
value: 0.5 ether,
|
|
166
|
-
decimals: 18,
|
|
167
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
168
|
-
}),
|
|
169
|
-
weight: 10e18,
|
|
170
|
-
newlyIssuedTokenCount: 0,
|
|
171
|
-
beneficiary: beneficiary,
|
|
172
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
173
|
-
payerMetadata: payerMetadata
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
vm.deal(mockTerminalAddress, 1 ether);
|
|
177
|
-
vm.prank(mockTerminalAddress);
|
|
178
|
-
testHook.afterPayRecordedWith{value: 0.5 ether}(payContext);
|
|
179
|
-
|
|
180
|
-
// Verify split hook was called.
|
|
181
|
-
assertEq(splitHook.callCount(), 1);
|
|
182
|
-
|
|
183
|
-
// Verify ETH was received.
|
|
184
|
-
assertEq(address(splitHook).balance, 0.5 ether);
|
|
185
|
-
|
|
186
|
-
// Verify context decimals.
|
|
187
|
-
assertEq(splitHook.lastDecimals(), 18);
|
|
188
|
-
assertEq(splitHook.lastAmount(), 0.5 ether);
|
|
189
|
-
assertEq(splitHook.lastToken(), JBConstants.NATIVE_TOKEN);
|
|
190
|
-
assertEq(splitHook.lastProjectId(), projectId);
|
|
191
|
-
assertEq(splitHook.lastGroupId(), groupId);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/// @notice Split hook + beneficiary split together with ETH.
|
|
195
|
-
function test_splitHook_eth_mixedWithBeneficiarySplit() public {
|
|
196
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
197
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
198
|
-
|
|
199
|
-
// Tier with 100% split, priced at 1 ETH.
|
|
200
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
201
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 1_000_000_000);
|
|
202
|
-
vm.prank(address(testHook));
|
|
203
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
204
|
-
|
|
205
|
-
mockAndExpect(
|
|
206
|
-
address(mockJBDirectory),
|
|
207
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
208
|
-
abi.encode(true)
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Two splits: 60% to hook, 40% to alice.
|
|
212
|
-
JBSplit[] memory splits = new JBSplit[](2);
|
|
213
|
-
splits[0] = JBSplit({
|
|
214
|
-
percent: uint32(600_000_000), // 60%
|
|
215
|
-
projectId: 0,
|
|
216
|
-
beneficiary: payable(address(0)),
|
|
217
|
-
preferAddToBalance: false,
|
|
218
|
-
lockedUntil: 0,
|
|
219
|
-
hook: IJBSplitHook(address(splitHook))
|
|
220
|
-
});
|
|
221
|
-
splits[1] = JBSplit({
|
|
222
|
-
percent: uint32(400_000_000), // 40% (of remaining)
|
|
223
|
-
projectId: 0,
|
|
224
|
-
beneficiary: payable(alice),
|
|
225
|
-
preferAddToBalance: false,
|
|
226
|
-
lockedUntil: 0,
|
|
227
|
-
hook: IJBSplitHook(address(0))
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
231
|
-
mockAndExpect(
|
|
232
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
236
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
237
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
238
|
-
|
|
239
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
240
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
241
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
242
|
-
splitAmounts[0] = 1 ether;
|
|
243
|
-
|
|
244
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
245
|
-
payer: beneficiary,
|
|
246
|
-
projectId: projectId,
|
|
247
|
-
rulesetId: 0,
|
|
248
|
-
amount: JBTokenAmount({
|
|
249
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
250
|
-
value: 1 ether,
|
|
251
|
-
decimals: 18,
|
|
252
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
253
|
-
}),
|
|
254
|
-
forwardedAmount: JBTokenAmount({
|
|
255
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
256
|
-
value: 1 ether,
|
|
257
|
-
decimals: 18,
|
|
258
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
259
|
-
}),
|
|
260
|
-
weight: 10e18,
|
|
261
|
-
newlyIssuedTokenCount: 0,
|
|
262
|
-
beneficiary: beneficiary,
|
|
263
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
264
|
-
payerMetadata: payerMetadata
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
uint256 aliceBalanceBefore = alice.balance;
|
|
268
|
-
|
|
269
|
-
vm.deal(mockTerminalAddress, 1 ether);
|
|
270
|
-
vm.prank(mockTerminalAddress);
|
|
271
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
272
|
-
|
|
273
|
-
// Hook gets 60% of 1 ETH = 0.6 ETH.
|
|
274
|
-
assertEq(address(splitHook).balance, 0.6 ether);
|
|
275
|
-
assertEq(splitHook.callCount(), 1);
|
|
276
|
-
assertEq(splitHook.lastDecimals(), 18);
|
|
277
|
-
|
|
278
|
-
// Alice gets 40% of remaining 0.4 ETH = 0.4 ETH.
|
|
279
|
-
// Note: second split is 400_000_000 / 400_000_000 (remaining percent) = 100% of leftover.
|
|
280
|
-
assertEq(alice.balance - aliceBalanceBefore, 0.4 ether);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// ───────────────────────────────────────────────────
|
|
284
|
-
// ERC20 / USDC Tests (6 decimals)
|
|
285
|
-
// ───────────────────────────────────────────────────
|
|
286
|
-
|
|
287
|
-
/// @notice Split hook receives USDC (6 decimals) with correct context.
|
|
288
|
-
function test_splitHook_usdc_receivesContextWith6Decimals() public {
|
|
289
|
-
MockERC20WithDecimals usdc = new MockERC20WithDecimals("USD Coin", "USDC", 6);
|
|
290
|
-
|
|
291
|
-
// Initialize hook with USDC pricing (6 decimals).
|
|
292
|
-
JB721TiersHook testHook = _initHookDefaultTiers(0, false, uint32(uint160(address(usdc))), 6);
|
|
293
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
294
|
-
|
|
295
|
-
// Tier priced at 100 USDC (100e6), 50% split.
|
|
296
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
297
|
-
tierConfigs[0] = _tierConfigWithSplit(100e6, 500_000_000);
|
|
298
|
-
vm.prank(address(testHook));
|
|
299
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
300
|
-
|
|
301
|
-
mockAndExpect(
|
|
302
|
-
address(mockJBDirectory),
|
|
303
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
304
|
-
abi.encode(true)
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
// Split: 100% to hook.
|
|
308
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
309
|
-
splits[0] = JBSplit({
|
|
310
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
311
|
-
projectId: 0,
|
|
312
|
-
beneficiary: payable(address(0)),
|
|
313
|
-
preferAddToBalance: false,
|
|
314
|
-
lockedUntil: 0,
|
|
315
|
-
hook: IJBSplitHook(address(splitHook))
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
319
|
-
mockAndExpect(
|
|
320
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
324
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
325
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
326
|
-
|
|
327
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
328
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
329
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
330
|
-
splitAmounts[0] = 50e6; // 50 USDC
|
|
331
|
-
|
|
332
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
333
|
-
payer: beneficiary,
|
|
334
|
-
projectId: projectId,
|
|
335
|
-
rulesetId: 0,
|
|
336
|
-
amount: JBTokenAmount({
|
|
337
|
-
token: address(usdc), value: 100e6, decimals: 6, currency: uint32(uint160(address(usdc)))
|
|
338
|
-
}),
|
|
339
|
-
forwardedAmount: JBTokenAmount({
|
|
340
|
-
token: address(usdc), value: 50e6, decimals: 6, currency: uint32(uint160(address(usdc)))
|
|
341
|
-
}),
|
|
342
|
-
weight: 10e18,
|
|
343
|
-
newlyIssuedTokenCount: 0,
|
|
344
|
-
beneficiary: beneficiary,
|
|
345
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
346
|
-
payerMetadata: payerMetadata
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
// Give terminal USDC and approve the hook.
|
|
350
|
-
usdc.mint(mockTerminalAddress, 100e6);
|
|
351
|
-
vm.prank(mockTerminalAddress);
|
|
352
|
-
usdc.approve(address(testHook), 50e6);
|
|
353
|
-
|
|
354
|
-
vm.prank(mockTerminalAddress);
|
|
355
|
-
testHook.afterPayRecordedWith(payContext);
|
|
356
|
-
|
|
357
|
-
// Verify split hook received USDC.
|
|
358
|
-
assertEq(usdc.balanceOf(address(splitHook)), 50e6);
|
|
359
|
-
assertEq(splitHook.callCount(), 1);
|
|
360
|
-
|
|
361
|
-
// Key assertion: decimals in context should be 6, not 18.
|
|
362
|
-
assertEq(splitHook.lastDecimals(), 6);
|
|
363
|
-
assertEq(splitHook.lastAmount(), 50e6);
|
|
364
|
-
assertEq(splitHook.lastToken(), address(usdc));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/// @notice USDC split with hook + beneficiary split alongside.
|
|
368
|
-
function test_splitHook_usdc_mixedWithBeneficiarySplit() public {
|
|
369
|
-
MockERC20WithDecimals usdc = new MockERC20WithDecimals("USD Coin", "USDC", 6);
|
|
370
|
-
|
|
371
|
-
JB721TiersHook testHook = _initHookDefaultTiers(0, false, uint32(uint160(address(usdc))), 6);
|
|
372
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
373
|
-
|
|
374
|
-
// Tier priced at 1000 USDC (1000e6), 100% split.
|
|
375
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
376
|
-
tierConfigs[0] = _tierConfigWithSplit(1000e6, 1_000_000_000);
|
|
377
|
-
vm.prank(address(testHook));
|
|
378
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
379
|
-
|
|
380
|
-
mockAndExpect(
|
|
381
|
-
address(mockJBDirectory),
|
|
382
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
383
|
-
abi.encode(true)
|
|
384
|
-
);
|
|
385
|
-
|
|
386
|
-
// Two splits: 70% to hook, 30% to bob.
|
|
387
|
-
JBSplit[] memory splits = new JBSplit[](2);
|
|
388
|
-
splits[0] = JBSplit({
|
|
389
|
-
percent: uint32(700_000_000), // 70%
|
|
390
|
-
projectId: 0,
|
|
391
|
-
beneficiary: payable(address(0)),
|
|
392
|
-
preferAddToBalance: false,
|
|
393
|
-
lockedUntil: 0,
|
|
394
|
-
hook: IJBSplitHook(address(splitHook))
|
|
395
|
-
});
|
|
396
|
-
splits[1] = JBSplit({
|
|
397
|
-
percent: uint32(300_000_000), // 30% (of remaining)
|
|
398
|
-
projectId: 0,
|
|
399
|
-
beneficiary: payable(bob),
|
|
400
|
-
preferAddToBalance: false,
|
|
401
|
-
lockedUntil: 0,
|
|
402
|
-
hook: IJBSplitHook(address(0))
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
406
|
-
mockAndExpect(
|
|
407
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
411
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
412
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
413
|
-
|
|
414
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
415
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
416
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
417
|
-
splitAmounts[0] = 1000e6;
|
|
418
|
-
|
|
419
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
420
|
-
payer: beneficiary,
|
|
421
|
-
projectId: projectId,
|
|
422
|
-
rulesetId: 0,
|
|
423
|
-
amount: JBTokenAmount({
|
|
424
|
-
token: address(usdc), value: 1000e6, decimals: 6, currency: uint32(uint160(address(usdc)))
|
|
425
|
-
}),
|
|
426
|
-
forwardedAmount: JBTokenAmount({
|
|
427
|
-
token: address(usdc), value: 1000e6, decimals: 6, currency: uint32(uint160(address(usdc)))
|
|
428
|
-
}),
|
|
429
|
-
weight: 10e18,
|
|
430
|
-
newlyIssuedTokenCount: 0,
|
|
431
|
-
beneficiary: beneficiary,
|
|
432
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
433
|
-
payerMetadata: payerMetadata
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
usdc.mint(mockTerminalAddress, 1000e6);
|
|
437
|
-
vm.prank(mockTerminalAddress);
|
|
438
|
-
usdc.approve(address(testHook), 1000e6);
|
|
439
|
-
|
|
440
|
-
vm.prank(mockTerminalAddress);
|
|
441
|
-
testHook.afterPayRecordedWith(payContext);
|
|
442
|
-
|
|
443
|
-
// Hook gets 70% of 1000 USDC = 700 USDC.
|
|
444
|
-
assertEq(usdc.balanceOf(address(splitHook)), 700e6);
|
|
445
|
-
assertEq(splitHook.lastDecimals(), 6);
|
|
446
|
-
|
|
447
|
-
// Bob gets 30% of remaining 300 USDC = 300 USDC.
|
|
448
|
-
assertEq(usdc.balanceOf(bob), 300e6);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
// ───────────────────────────────────────────────────
|
|
452
|
-
// Split hook priority tests
|
|
453
|
-
// ───────────────────────────────────────────────────
|
|
454
|
-
|
|
455
|
-
/// @notice Split hook takes priority over projectId.
|
|
456
|
-
function test_splitHook_takesPriorityOverProjectId() public {
|
|
457
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
458
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
459
|
-
|
|
460
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
461
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 1_000_000_000);
|
|
462
|
-
vm.prank(address(testHook));
|
|
463
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
464
|
-
|
|
465
|
-
mockAndExpect(
|
|
466
|
-
address(mockJBDirectory),
|
|
467
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
468
|
-
abi.encode(true)
|
|
469
|
-
);
|
|
470
|
-
|
|
471
|
-
// Split has BOTH hook AND projectId — hook should take priority.
|
|
472
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
473
|
-
splits[0] = JBSplit({
|
|
474
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
475
|
-
projectId: uint64(99), // Also has a project ID
|
|
476
|
-
beneficiary: payable(alice), // Also has a beneficiary
|
|
477
|
-
preferAddToBalance: false,
|
|
478
|
-
lockedUntil: 0,
|
|
479
|
-
hook: IJBSplitHook(address(splitHook)) // Hook set — should take priority
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
483
|
-
mockAndExpect(
|
|
484
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
485
|
-
);
|
|
486
|
-
|
|
487
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
488
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
489
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
490
|
-
|
|
491
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
492
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
493
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
494
|
-
splitAmounts[0] = 1 ether;
|
|
495
|
-
|
|
496
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
497
|
-
payer: beneficiary,
|
|
498
|
-
projectId: projectId,
|
|
499
|
-
rulesetId: 0,
|
|
500
|
-
amount: JBTokenAmount({
|
|
501
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
502
|
-
value: 1 ether,
|
|
503
|
-
decimals: 18,
|
|
504
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
505
|
-
}),
|
|
506
|
-
forwardedAmount: JBTokenAmount({
|
|
507
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
508
|
-
value: 1 ether,
|
|
509
|
-
decimals: 18,
|
|
510
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
511
|
-
}),
|
|
512
|
-
weight: 10e18,
|
|
513
|
-
newlyIssuedTokenCount: 0,
|
|
514
|
-
beneficiary: beneficiary,
|
|
515
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
516
|
-
payerMetadata: payerMetadata
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
vm.deal(mockTerminalAddress, 1 ether);
|
|
520
|
-
vm.prank(mockTerminalAddress);
|
|
521
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
522
|
-
|
|
523
|
-
// Hook received the ETH (not the project or alice).
|
|
524
|
-
assertEq(address(splitHook).balance, 1 ether);
|
|
525
|
-
assertEq(splitHook.callCount(), 1);
|
|
526
|
-
// Alice got nothing.
|
|
527
|
-
assertEq(alice.balance, 0);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/// @notice Verifies the split context contains the correct split struct.
|
|
531
|
-
function test_splitHook_contextContainsCorrectSplit() public {
|
|
532
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
533
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
534
|
-
|
|
535
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
536
|
-
tierConfigs[0] = _tierConfigWithSplit(1 ether, 1_000_000_000);
|
|
537
|
-
vm.prank(address(testHook));
|
|
538
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
539
|
-
|
|
540
|
-
mockAndExpect(
|
|
541
|
-
address(mockJBDirectory),
|
|
542
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
543
|
-
abi.encode(true)
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
547
|
-
splits[0] = JBSplit({
|
|
548
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
549
|
-
projectId: uint64(42),
|
|
550
|
-
beneficiary: payable(bob),
|
|
551
|
-
preferAddToBalance: true,
|
|
552
|
-
lockedUntil: 0,
|
|
553
|
-
hook: IJBSplitHook(address(splitHook))
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
557
|
-
mockAndExpect(
|
|
558
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
559
|
-
);
|
|
560
|
-
|
|
561
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
562
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
563
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
564
|
-
|
|
565
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
566
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
567
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
568
|
-
splitAmounts[0] = 1 ether;
|
|
569
|
-
|
|
570
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
571
|
-
payer: beneficiary,
|
|
572
|
-
projectId: projectId,
|
|
573
|
-
rulesetId: 0,
|
|
574
|
-
amount: JBTokenAmount({
|
|
575
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
576
|
-
value: 1 ether,
|
|
577
|
-
decimals: 18,
|
|
578
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
579
|
-
}),
|
|
580
|
-
forwardedAmount: JBTokenAmount({
|
|
581
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
582
|
-
value: 1 ether,
|
|
583
|
-
decimals: 18,
|
|
584
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
585
|
-
}),
|
|
586
|
-
weight: 10e18,
|
|
587
|
-
newlyIssuedTokenCount: 0,
|
|
588
|
-
beneficiary: beneficiary,
|
|
589
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
590
|
-
payerMetadata: payerMetadata
|
|
591
|
-
});
|
|
592
|
-
|
|
593
|
-
vm.deal(mockTerminalAddress, 1 ether);
|
|
594
|
-
vm.prank(mockTerminalAddress);
|
|
595
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
596
|
-
|
|
597
|
-
// Verify split struct was passed through correctly.
|
|
598
|
-
(, uint64 pid, address payable ben, bool pref,, IJBSplitHook hk) = splitHook.lastSplit();
|
|
599
|
-
assertEq(pid, 42);
|
|
600
|
-
assertEq(ben, bob);
|
|
601
|
-
assertEq(pref, true);
|
|
602
|
-
assertEq(address(hk), address(splitHook));
|
|
603
|
-
}
|
|
604
|
-
}
|