@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,751 +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 {IJB721TiersHook} from "../../src/interfaces/IJB721TiersHook.sol";
|
|
7
|
-
import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
|
|
8
|
-
import {JB721TiersHookStore} from "../../src/JB721TiersHookStore.sol";
|
|
9
|
-
// forge-lint: disable-next-line(unused-import)
|
|
10
|
-
import {JB721TiersHookLib} from "../../src/libraries/JB721TiersHookLib.sol";
|
|
11
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
12
|
-
import {JBSplitHookContext} from "@bananapus/core-v6/src/structs/JBSplitHookContext.sol";
|
|
13
|
-
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
14
|
-
import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
|
|
15
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
16
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
17
|
-
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
|
18
|
-
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
19
|
-
|
|
20
|
-
/// @notice Regression tests for split distribution bugs in JB721TiersHookLib.
|
|
21
|
-
contract Test_SplitDistributionBugs is UnitTestSetup {
|
|
22
|
-
using stdStorage for StdStorage;
|
|
23
|
-
|
|
24
|
-
address alice = makeAddr("alice");
|
|
25
|
-
address bob = makeAddr("bob");
|
|
26
|
-
|
|
27
|
-
function setUp() public override {
|
|
28
|
-
super.setUp();
|
|
29
|
-
vm.etch(mockJBSplits, new bytes(0x69));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Helper: build payer metadata for tier IDs.
|
|
33
|
-
function _buildPayerMetadata(
|
|
34
|
-
address hookAddress,
|
|
35
|
-
uint16[] memory tierIdsToMint
|
|
36
|
-
)
|
|
37
|
-
internal
|
|
38
|
-
view
|
|
39
|
-
returns (bytes memory)
|
|
40
|
-
{
|
|
41
|
-
bytes[] memory data = new bytes[](1);
|
|
42
|
-
data[0] = abi.encode(false, tierIdsToMint);
|
|
43
|
-
bytes4[] memory ids = new bytes4[](1);
|
|
44
|
-
ids[0] = metadataHelper.getId("pay", hookAddress);
|
|
45
|
-
return metadataHelper.createMetadata(ids, data);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
49
|
-
// Split underflow DoS: _distributeSingleSplit with 2+ splits
|
|
50
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
/// @notice When two splits each get 50% of a tier's funds, the old code used `amount`
|
|
53
|
-
/// (the original total) in mulDiv for every split. After the first split consumed half
|
|
54
|
-
/// the funds, the second split would compute its payout from the original `amount`,
|
|
55
|
-
/// yielding a value that exceeds `leftoverAmount`. The unchecked subtraction would
|
|
56
|
-
/// underflow, causing a revert (DoS).
|
|
57
|
-
///
|
|
58
|
-
/// With the fix (using `leftoverAmount` instead of `amount`), the second split
|
|
59
|
-
/// correctly computes its payout from the remaining funds and the distribution succeeds.
|
|
60
|
-
function test_splitDistribution_twoSplits_usesLeftoverAmount_noUnderflow() public {
|
|
61
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
62
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
63
|
-
|
|
64
|
-
// Add a tier with 100% split, priced at 1 ETH.
|
|
65
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
66
|
-
tierConfigs[0].price = 1 ether;
|
|
67
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
68
|
-
tierConfigs[0].category = uint24(1);
|
|
69
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
70
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
71
|
-
|
|
72
|
-
vm.prank(address(testHook));
|
|
73
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
74
|
-
|
|
75
|
-
// Mock directory checks.
|
|
76
|
-
mockAndExpect(
|
|
77
|
-
address(mockJBDirectory),
|
|
78
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
79
|
-
abi.encode(true)
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
// Mock splits: TWO beneficiaries each with 50%.
|
|
83
|
-
JBSplit[] memory splits = new JBSplit[](2);
|
|
84
|
-
splits[0] = JBSplit({
|
|
85
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2), // 50%
|
|
86
|
-
projectId: 0,
|
|
87
|
-
beneficiary: payable(alice),
|
|
88
|
-
preferAddToBalance: false,
|
|
89
|
-
lockedUntil: 0,
|
|
90
|
-
hook: IJBSplitHook(address(0))
|
|
91
|
-
});
|
|
92
|
-
splits[1] = JBSplit({
|
|
93
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2), // 50%
|
|
94
|
-
projectId: 0,
|
|
95
|
-
beneficiary: payable(bob),
|
|
96
|
-
preferAddToBalance: false,
|
|
97
|
-
lockedUntil: 0,
|
|
98
|
-
hook: IJBSplitHook(address(0))
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
102
|
-
mockAndExpect(
|
|
103
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
// Build payer metadata.
|
|
107
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
108
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
109
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
110
|
-
|
|
111
|
-
// Build hook metadata (per-tier split breakdown).
|
|
112
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
113
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
114
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
115
|
-
splitAmounts[0] = 1 ether; // Full tier price as split amount.
|
|
116
|
-
|
|
117
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
118
|
-
payer: beneficiary,
|
|
119
|
-
projectId: projectId,
|
|
120
|
-
rulesetId: 0,
|
|
121
|
-
amount: JBTokenAmount({
|
|
122
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
123
|
-
value: 1 ether,
|
|
124
|
-
decimals: 18,
|
|
125
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
126
|
-
}),
|
|
127
|
-
forwardedAmount: JBTokenAmount({
|
|
128
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
129
|
-
value: 1 ether,
|
|
130
|
-
decimals: 18,
|
|
131
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
132
|
-
}),
|
|
133
|
-
weight: 10e18,
|
|
134
|
-
newlyIssuedTokenCount: 0,
|
|
135
|
-
beneficiary: beneficiary,
|
|
136
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
137
|
-
payerMetadata: payerMetadata
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
uint256 aliceBalanceBefore = alice.balance;
|
|
141
|
-
uint256 bobBalanceBefore = bob.balance;
|
|
142
|
-
|
|
143
|
-
vm.deal(mockTerminalAddress, 2 ether);
|
|
144
|
-
vm.prank(mockTerminalAddress);
|
|
145
|
-
// This would revert with the old code due to underflow. With the fix it succeeds.
|
|
146
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
147
|
-
|
|
148
|
-
// Alice and Bob should each receive 0.5 ETH.
|
|
149
|
-
assertEq(alice.balance - aliceBalanceBefore, 0.5 ether, "Alice should receive 0.5 ETH");
|
|
150
|
-
assertEq(bob.balance - bobBalanceBefore, 0.5 ether, "Bob should receive 0.5 ETH");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
154
|
-
// Split amounts must use discounted tier price, not full price
|
|
155
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
156
|
-
|
|
157
|
-
/// @notice When a tier has a discount, `calculateSplitAmounts()` should use the
|
|
158
|
-
/// discounted price (matching what `recordMint` charges), not the full undiscounted
|
|
159
|
-
/// tier price. Without the fix, the split amount would be inflated.
|
|
160
|
-
function test_calculateSplitAmounts_usesDiscountedPrice() public {
|
|
161
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
162
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
163
|
-
|
|
164
|
-
// Add a tier with 50% discount and 100% split, priced at 1 ETH.
|
|
165
|
-
// discountPercent=100 out of DISCOUNT_DENOMINATOR=200 means a 50% discount.
|
|
166
|
-
// So the effective price should be 0.5 ETH.
|
|
167
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
168
|
-
tierConfigs[0].price = 1 ether;
|
|
169
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
170
|
-
tierConfigs[0].category = uint24(1);
|
|
171
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
172
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
173
|
-
tierConfigs[0].discountPercent = 100; // 50% discount (100/200)
|
|
174
|
-
|
|
175
|
-
vm.prank(address(testHook));
|
|
176
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
177
|
-
|
|
178
|
-
// Build payer metadata requesting that tier.
|
|
179
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
180
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
181
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
182
|
-
|
|
183
|
-
// Call calculateSplitAmounts via beforePayRecordedWith.
|
|
184
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
185
|
-
terminal: mockTerminalAddress,
|
|
186
|
-
payer: beneficiary,
|
|
187
|
-
amount: JBTokenAmount({
|
|
188
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
189
|
-
value: 1 ether,
|
|
190
|
-
decimals: 18,
|
|
191
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
192
|
-
}),
|
|
193
|
-
projectId: projectId,
|
|
194
|
-
rulesetId: 0,
|
|
195
|
-
beneficiary: beneficiary,
|
|
196
|
-
weight: 10e18,
|
|
197
|
-
reservedPercent: 5000,
|
|
198
|
-
metadata: payerMetadata
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
202
|
-
|
|
203
|
-
// With 50% discount on a 1 ETH tier and 100% split, the split amount should be 0.5 ETH.
|
|
204
|
-
// Without the fix, it would be 1 ETH (using the full undiscounted price).
|
|
205
|
-
assertEq(specs[0].amount, 0.5 ether, "Split amount should use discounted price (0.5 ETH, not 1 ETH)");
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/// @notice Verify the discounted split amount math matches what recordMint charges.
|
|
209
|
-
/// A tier priced at 2 ETH with a 25% discount (discountPercent=50, denominator=200)
|
|
210
|
-
/// has effective price 1.5 ETH. With 50% split, the split amount should be 0.75 ETH.
|
|
211
|
-
function test_calculateSplitAmounts_partialDiscount_partialSplit() public {
|
|
212
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
213
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
214
|
-
|
|
215
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
216
|
-
tierConfigs[0].price = 2 ether;
|
|
217
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
218
|
-
tierConfigs[0].category = uint24(1);
|
|
219
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
220
|
-
tierConfigs[0].splitPercent = 500_000_000; // 50%
|
|
221
|
-
tierConfigs[0].discountPercent = 50; // 25% discount (50/200)
|
|
222
|
-
|
|
223
|
-
vm.prank(address(testHook));
|
|
224
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
225
|
-
|
|
226
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
227
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
228
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
229
|
-
|
|
230
|
-
JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
|
|
231
|
-
terminal: mockTerminalAddress,
|
|
232
|
-
payer: beneficiary,
|
|
233
|
-
amount: JBTokenAmount({
|
|
234
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
235
|
-
value: 2 ether,
|
|
236
|
-
decimals: 18,
|
|
237
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
238
|
-
}),
|
|
239
|
-
projectId: projectId,
|
|
240
|
-
rulesetId: 0,
|
|
241
|
-
beneficiary: beneficiary,
|
|
242
|
-
weight: 10e18,
|
|
243
|
-
reservedPercent: 5000,
|
|
244
|
-
metadata: payerMetadata
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
(, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
|
|
248
|
-
|
|
249
|
-
// effectivePrice = 2 ETH - (2 ETH * 50 / 200) = 2 ETH - 0.5 ETH = 1.5 ETH
|
|
250
|
-
// splitAmount = 1.5 ETH * 50% = 0.75 ETH
|
|
251
|
-
assertEq(specs[0].amount, 0.75 ether, "Split amount should be 0.75 ETH with 25% discount and 50% split");
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
255
|
-
// Failed ETH send to split beneficiary should not revert entire payment
|
|
256
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
257
|
-
|
|
258
|
-
/// @notice When an ETH send to a split beneficiary fails (e.g., the beneficiary is a
|
|
259
|
-
/// contract that reverts on receive), the old code propagated the revert, causing the
|
|
260
|
-
/// entire payment to fail. With the fix, the failed send returns false, and the funds
|
|
261
|
-
/// are routed to the project's balance instead.
|
|
262
|
-
function test_revertingSplitBeneficiary_routesToProjectBalance() public {
|
|
263
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
264
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
265
|
-
|
|
266
|
-
// Add a tier with 100% split.
|
|
267
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
268
|
-
tierConfigs[0].price = 1 ether;
|
|
269
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
270
|
-
tierConfigs[0].category = uint24(1);
|
|
271
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
272
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
273
|
-
|
|
274
|
-
vm.prank(address(testHook));
|
|
275
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
276
|
-
|
|
277
|
-
// Mock directory checks.
|
|
278
|
-
mockAndExpect(
|
|
279
|
-
address(mockJBDirectory),
|
|
280
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
281
|
-
abi.encode(true)
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
// Deploy a contract that always reverts when receiving ETH.
|
|
285
|
-
RevertOnReceive revertingBeneficiary = new RevertOnReceive();
|
|
286
|
-
|
|
287
|
-
// Mock splits: 100% to a beneficiary that reverts on ETH receive.
|
|
288
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
289
|
-
splits[0] = JBSplit({
|
|
290
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
291
|
-
projectId: 0,
|
|
292
|
-
beneficiary: payable(address(revertingBeneficiary)),
|
|
293
|
-
preferAddToBalance: false,
|
|
294
|
-
lockedUntil: 0,
|
|
295
|
-
hook: IJBSplitHook(address(0))
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
299
|
-
mockAndExpect(
|
|
300
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
301
|
-
);
|
|
302
|
-
|
|
303
|
-
// Mock the project's primary terminal for the fallback addToBalance.
|
|
304
|
-
address projectTerminal = makeAddr("projectTerminal");
|
|
305
|
-
vm.etch(projectTerminal, new bytes(0x69));
|
|
306
|
-
mockAndExpect(
|
|
307
|
-
address(mockJBDirectory),
|
|
308
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
|
|
309
|
-
abi.encode(projectTerminal)
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
// Expect addToBalanceOf to be called on the project's terminal with the full 1 ETH
|
|
313
|
-
// (since the beneficiary failed, funds route to project balance).
|
|
314
|
-
vm.expectCall(
|
|
315
|
-
projectTerminal,
|
|
316
|
-
1 ether,
|
|
317
|
-
abi.encodeWithSelector(
|
|
318
|
-
IJBTerminal.addToBalanceOf.selector, projectId, JBConstants.NATIVE_TOKEN, 1 ether, false, "", ""
|
|
319
|
-
)
|
|
320
|
-
);
|
|
321
|
-
vm.mockCall(projectTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
|
|
322
|
-
|
|
323
|
-
// Build payer metadata.
|
|
324
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
325
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
326
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
327
|
-
|
|
328
|
-
// Build hook metadata.
|
|
329
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
330
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
331
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
332
|
-
splitAmounts[0] = 1 ether;
|
|
333
|
-
|
|
334
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
335
|
-
payer: beneficiary,
|
|
336
|
-
projectId: projectId,
|
|
337
|
-
rulesetId: 0,
|
|
338
|
-
amount: JBTokenAmount({
|
|
339
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
340
|
-
value: 1 ether,
|
|
341
|
-
decimals: 18,
|
|
342
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
343
|
-
}),
|
|
344
|
-
forwardedAmount: JBTokenAmount({
|
|
345
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
346
|
-
value: 1 ether,
|
|
347
|
-
decimals: 18,
|
|
348
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
349
|
-
}),
|
|
350
|
-
weight: 10e18,
|
|
351
|
-
newlyIssuedTokenCount: 0,
|
|
352
|
-
beneficiary: beneficiary,
|
|
353
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
354
|
-
payerMetadata: payerMetadata
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
vm.deal(mockTerminalAddress, 2 ether);
|
|
358
|
-
vm.prank(mockTerminalAddress);
|
|
359
|
-
// With the old code this would revert. With the fix, it succeeds and routes to project balance.
|
|
360
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
361
|
-
|
|
362
|
-
// Verify the reverting beneficiary received nothing.
|
|
363
|
-
assertEq(address(revertingBeneficiary).balance, 0, "Reverting beneficiary should receive nothing");
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
367
|
-
// Reverting split hook (ETH) should not brick payments
|
|
368
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
369
|
-
|
|
370
|
-
/// @notice A reverting split hook (ETH path) should not revert the entire payment.
|
|
371
|
-
/// The try-catch catches the revert, returns false, and funds route to project balance.
|
|
372
|
-
function test_revertingSplitHook_eth_routesToProjectBalance() public {
|
|
373
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
374
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
375
|
-
|
|
376
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
377
|
-
tierConfigs[0].price = 1 ether;
|
|
378
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
379
|
-
tierConfigs[0].category = uint24(1);
|
|
380
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
381
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
382
|
-
|
|
383
|
-
vm.prank(address(testHook));
|
|
384
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
385
|
-
|
|
386
|
-
mockAndExpect(
|
|
387
|
-
address(mockJBDirectory),
|
|
388
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
389
|
-
abi.encode(true)
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
// Deploy a split hook that always reverts.
|
|
393
|
-
RevertingSplitHook revertingHook = new RevertingSplitHook();
|
|
394
|
-
|
|
395
|
-
// Split: 100% to the reverting hook.
|
|
396
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
397
|
-
splits[0] = JBSplit({
|
|
398
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
399
|
-
projectId: 0,
|
|
400
|
-
beneficiary: payable(address(0)),
|
|
401
|
-
preferAddToBalance: false,
|
|
402
|
-
lockedUntil: 0,
|
|
403
|
-
hook: IJBSplitHook(address(revertingHook))
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
407
|
-
mockAndExpect(
|
|
408
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
409
|
-
);
|
|
410
|
-
|
|
411
|
-
// Mock the project's primary terminal for the fallback addToBalance.
|
|
412
|
-
address projectTerminal = makeAddr("projectTerminal");
|
|
413
|
-
vm.etch(projectTerminal, new bytes(0x69));
|
|
414
|
-
mockAndExpect(
|
|
415
|
-
address(mockJBDirectory),
|
|
416
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
|
|
417
|
-
abi.encode(projectTerminal)
|
|
418
|
-
);
|
|
419
|
-
|
|
420
|
-
// Expect addToBalanceOf on the project's terminal with the full 1 ETH.
|
|
421
|
-
vm.expectCall(
|
|
422
|
-
projectTerminal,
|
|
423
|
-
1 ether,
|
|
424
|
-
abi.encodeWithSelector(
|
|
425
|
-
IJBTerminal.addToBalanceOf.selector, projectId, JBConstants.NATIVE_TOKEN, 1 ether, false, "", ""
|
|
426
|
-
)
|
|
427
|
-
);
|
|
428
|
-
vm.mockCall(projectTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
|
|
429
|
-
|
|
430
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
431
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
432
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
433
|
-
|
|
434
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
435
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
436
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
437
|
-
splitAmounts[0] = 1 ether;
|
|
438
|
-
|
|
439
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
440
|
-
payer: beneficiary,
|
|
441
|
-
projectId: projectId,
|
|
442
|
-
rulesetId: 0,
|
|
443
|
-
amount: JBTokenAmount({
|
|
444
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
445
|
-
value: 1 ether,
|
|
446
|
-
decimals: 18,
|
|
447
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
448
|
-
}),
|
|
449
|
-
forwardedAmount: JBTokenAmount({
|
|
450
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
451
|
-
value: 1 ether,
|
|
452
|
-
decimals: 18,
|
|
453
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
454
|
-
}),
|
|
455
|
-
weight: 10e18,
|
|
456
|
-
newlyIssuedTokenCount: 0,
|
|
457
|
-
beneficiary: beneficiary,
|
|
458
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
459
|
-
payerMetadata: payerMetadata
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
// Expect the SplitPayoutReverted event (check only the indexed projectId topic).
|
|
463
|
-
vm.expectEmit(true, false, false, false);
|
|
464
|
-
emit IJB721TiersHook.SplitPayoutReverted(projectId, splits[0], 1 ether, "", mockTerminalAddress);
|
|
465
|
-
|
|
466
|
-
vm.deal(mockTerminalAddress, 2 ether);
|
|
467
|
-
vm.prank(mockTerminalAddress);
|
|
468
|
-
// Should NOT revert — the try-catch catches the hook's revert.
|
|
469
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
470
|
-
|
|
471
|
-
// Reverting hook should have received nothing.
|
|
472
|
-
assertEq(address(revertingHook).balance, 0, "Reverting hook should receive no ETH");
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
476
|
-
// Reverting split hook (ERC-20) should not brick payments
|
|
477
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
478
|
-
|
|
479
|
-
/// @notice A reverting ERC-20 split hook should NOT block the payment. Tokens are transferred
|
|
480
|
-
/// via safeTransfer BEFORE the callback, so the function must return true regardless of
|
|
481
|
-
/// callback success. The hook receives the tokens even though its callback reverted.
|
|
482
|
-
function test_revertingSplitHook_erc20_tokensTransferredCallbackIgnored() public {
|
|
483
|
-
MintableERC20 usdc = new MintableERC20("USD Coin", "USDC", 6);
|
|
484
|
-
uint32 usdcCurrency = uint32(uint160(address(usdc)));
|
|
485
|
-
|
|
486
|
-
JB721TiersHook testHook = _initHookDefaultTiers(0, false, usdcCurrency, 6);
|
|
487
|
-
|
|
488
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
489
|
-
tierConfigs[0].price = 100e6;
|
|
490
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
491
|
-
tierConfigs[0].category = uint24(1);
|
|
492
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
493
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
494
|
-
|
|
495
|
-
vm.prank(address(testHook));
|
|
496
|
-
uint256[] memory tierIds = testHook.STORE().recordAddTiers(tierConfigs);
|
|
497
|
-
|
|
498
|
-
mockAndExpect(
|
|
499
|
-
address(mockJBDirectory),
|
|
500
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
501
|
-
abi.encode(true)
|
|
502
|
-
);
|
|
503
|
-
|
|
504
|
-
RevertingSplitHook revertingHook = new RevertingSplitHook();
|
|
505
|
-
|
|
506
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
507
|
-
splits[0] = JBSplit({
|
|
508
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
509
|
-
projectId: 0,
|
|
510
|
-
beneficiary: payable(address(0)),
|
|
511
|
-
preferAddToBalance: false,
|
|
512
|
-
lockedUntil: 0,
|
|
513
|
-
hook: IJBSplitHook(address(revertingHook))
|
|
514
|
-
});
|
|
515
|
-
|
|
516
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
517
|
-
mockAndExpect(
|
|
518
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
JBAfterPayRecordedContext memory payContext =
|
|
522
|
-
_buildErc20PayContext(address(testHook), address(usdc), usdcCurrency, 6, tierIds, 100e6);
|
|
523
|
-
|
|
524
|
-
usdc.mint(mockTerminalAddress, 100e6);
|
|
525
|
-
vm.prank(mockTerminalAddress);
|
|
526
|
-
usdc.approve(address(testHook), 100e6);
|
|
527
|
-
|
|
528
|
-
// Expect the SplitPayoutReverted event (emitted even though tokens were transferred).
|
|
529
|
-
vm.expectEmit(true, false, false, false);
|
|
530
|
-
emit IJB721TiersHook.SplitPayoutReverted(projectId, splits[0], 100e6, "", mockTerminalAddress);
|
|
531
|
-
|
|
532
|
-
vm.prank(mockTerminalAddress);
|
|
533
|
-
testHook.afterPayRecordedWith(payContext);
|
|
534
|
-
|
|
535
|
-
assertEq(usdc.balanceOf(address(revertingHook)), 100e6, "Hook should receive ERC20 despite callback revert");
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/// @notice Helper to build an ERC-20 afterPay context (reduces stack depth).
|
|
539
|
-
function _buildErc20PayContext(
|
|
540
|
-
address hookAddress,
|
|
541
|
-
address token,
|
|
542
|
-
uint32 currency,
|
|
543
|
-
uint8 decimals,
|
|
544
|
-
uint256[] memory tierIds,
|
|
545
|
-
uint256 amount
|
|
546
|
-
)
|
|
547
|
-
internal
|
|
548
|
-
view
|
|
549
|
-
returns (JBAfterPayRecordedContext memory)
|
|
550
|
-
{
|
|
551
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
552
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
553
|
-
bytes memory payerMetadata = _buildPayerMetadata(hookAddress, mintIds);
|
|
554
|
-
|
|
555
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
556
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
557
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
558
|
-
splitAmounts[0] = amount;
|
|
559
|
-
|
|
560
|
-
return JBAfterPayRecordedContext({
|
|
561
|
-
payer: beneficiary,
|
|
562
|
-
projectId: projectId,
|
|
563
|
-
rulesetId: 0,
|
|
564
|
-
amount: JBTokenAmount({token: token, value: amount, decimals: decimals, currency: currency}),
|
|
565
|
-
forwardedAmount: JBTokenAmount({token: token, value: amount, decimals: decimals, currency: currency}),
|
|
566
|
-
weight: 10e18,
|
|
567
|
-
newlyIssuedTokenCount: 0,
|
|
568
|
-
beneficiary: beneficiary,
|
|
569
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
570
|
-
payerMetadata: payerMetadata
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
575
|
-
// Reverting terminal (ETH) should not brick payments
|
|
576
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
577
|
-
|
|
578
|
-
/// @notice When a split targets a project whose terminal reverts on pay(), the try-catch
|
|
579
|
-
/// catches the revert, returns false, and funds route to the project's own balance.
|
|
580
|
-
function test_revertingTerminal_eth_routesToProjectBalance() public {
|
|
581
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
582
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
583
|
-
|
|
584
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
585
|
-
tierConfigs[0].price = 1 ether;
|
|
586
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
587
|
-
tierConfigs[0].category = uint24(1);
|
|
588
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
589
|
-
tierConfigs[0].splitPercent = 1_000_000_000; // 100%
|
|
590
|
-
|
|
591
|
-
vm.prank(address(testHook));
|
|
592
|
-
uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
|
|
593
|
-
|
|
594
|
-
mockAndExpect(
|
|
595
|
-
address(mockJBDirectory),
|
|
596
|
-
abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
|
|
597
|
-
abi.encode(true)
|
|
598
|
-
);
|
|
599
|
-
|
|
600
|
-
// Set up a split targeting project 99 (whose terminal will revert).
|
|
601
|
-
uint64 targetProjectId = 99;
|
|
602
|
-
|
|
603
|
-
// Mock primaryTerminalOf for project 99 to return a reverting terminal.
|
|
604
|
-
address revertingTerminal = makeAddr("revertingTerminal");
|
|
605
|
-
vm.etch(revertingTerminal, new bytes(0x69));
|
|
606
|
-
mockAndExpect(
|
|
607
|
-
address(mockJBDirectory),
|
|
608
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, targetProjectId, JBConstants.NATIVE_TOKEN),
|
|
609
|
-
abi.encode(revertingTerminal)
|
|
610
|
-
);
|
|
611
|
-
|
|
612
|
-
// Make the terminal's pay() revert.
|
|
613
|
-
vm.mockCallRevert(revertingTerminal, abi.encodeWithSelector(IJBTerminal.pay.selector), "terminal broken");
|
|
614
|
-
|
|
615
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
616
|
-
splits[0] = JBSplit({
|
|
617
|
-
percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
|
|
618
|
-
projectId: targetProjectId,
|
|
619
|
-
beneficiary: payable(alice),
|
|
620
|
-
preferAddToBalance: false,
|
|
621
|
-
lockedUntil: 0,
|
|
622
|
-
hook: IJBSplitHook(address(0))
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
|
|
626
|
-
mockAndExpect(
|
|
627
|
-
mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
|
|
628
|
-
);
|
|
629
|
-
|
|
630
|
-
// Mock the hook's own project's terminal for fallback addToBalance.
|
|
631
|
-
address projectTerminal = makeAddr("projectTerminal");
|
|
632
|
-
vm.etch(projectTerminal, new bytes(0x69));
|
|
633
|
-
mockAndExpect(
|
|
634
|
-
address(mockJBDirectory),
|
|
635
|
-
abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
|
|
636
|
-
abi.encode(projectTerminal)
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
vm.expectCall(
|
|
640
|
-
projectTerminal,
|
|
641
|
-
1 ether,
|
|
642
|
-
abi.encodeWithSelector(
|
|
643
|
-
IJBTerminal.addToBalanceOf.selector, projectId, JBConstants.NATIVE_TOKEN, 1 ether, false, "", ""
|
|
644
|
-
)
|
|
645
|
-
);
|
|
646
|
-
vm.mockCall(projectTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), abi.encode());
|
|
647
|
-
|
|
648
|
-
uint16[] memory mintIds = new uint16[](1);
|
|
649
|
-
mintIds[0] = uint16(tierIds[0]);
|
|
650
|
-
bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
|
|
651
|
-
|
|
652
|
-
uint16[] memory splitTierIds = new uint16[](1);
|
|
653
|
-
splitTierIds[0] = uint16(tierIds[0]);
|
|
654
|
-
uint256[] memory splitAmounts = new uint256[](1);
|
|
655
|
-
splitAmounts[0] = 1 ether;
|
|
656
|
-
|
|
657
|
-
JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
|
|
658
|
-
payer: beneficiary,
|
|
659
|
-
projectId: projectId,
|
|
660
|
-
rulesetId: 0,
|
|
661
|
-
amount: JBTokenAmount({
|
|
662
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
663
|
-
value: 1 ether,
|
|
664
|
-
decimals: 18,
|
|
665
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
666
|
-
}),
|
|
667
|
-
forwardedAmount: JBTokenAmount({
|
|
668
|
-
token: JBConstants.NATIVE_TOKEN,
|
|
669
|
-
value: 1 ether,
|
|
670
|
-
decimals: 18,
|
|
671
|
-
currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
672
|
-
}),
|
|
673
|
-
weight: 10e18,
|
|
674
|
-
newlyIssuedTokenCount: 0,
|
|
675
|
-
beneficiary: beneficiary,
|
|
676
|
-
hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
|
|
677
|
-
payerMetadata: payerMetadata
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
// Expect the SplitPayoutReverted event.
|
|
681
|
-
vm.expectEmit(true, false, false, false);
|
|
682
|
-
emit IJB721TiersHook.SplitPayoutReverted(projectId, splits[0], 1 ether, "", mockTerminalAddress);
|
|
683
|
-
|
|
684
|
-
vm.deal(mockTerminalAddress, 2 ether);
|
|
685
|
-
vm.prank(mockTerminalAddress);
|
|
686
|
-
// Should NOT revert — terminal failure is caught, funds go to project balance.
|
|
687
|
-
testHook.afterPayRecordedWith{value: 1 ether}(payContext);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
691
|
-
// splitPercent validation in store
|
|
692
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
693
|
-
|
|
694
|
-
/// @notice Adding a tier with splitPercent > SPLITS_TOTAL_PERCENT should revert.
|
|
695
|
-
function test_recordAddTiers_splitPercentExceedsBounds_reverts() public {
|
|
696
|
-
ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
|
|
697
|
-
IJB721TiersHookStore hookStore = testHook.STORE();
|
|
698
|
-
|
|
699
|
-
JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
|
|
700
|
-
tierConfigs[0].price = 1 ether;
|
|
701
|
-
tierConfigs[0].initialSupply = uint32(100);
|
|
702
|
-
tierConfigs[0].category = uint24(1);
|
|
703
|
-
tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
|
|
704
|
-
tierConfigs[0].splitPercent = JBConstants.SPLITS_TOTAL_PERCENT + 1; // Over the limit.
|
|
705
|
-
|
|
706
|
-
vm.prank(address(testHook));
|
|
707
|
-
vm.expectRevert(
|
|
708
|
-
abi.encodeWithSelector(
|
|
709
|
-
JB721TiersHookStore.JB721TiersHookStore_SplitPercentExceedsBounds.selector,
|
|
710
|
-
JBConstants.SPLITS_TOTAL_PERCENT + 1,
|
|
711
|
-
JBConstants.SPLITS_TOTAL_PERCENT
|
|
712
|
-
)
|
|
713
|
-
);
|
|
714
|
-
hookStore.recordAddTiers(tierConfigs);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
/// @notice A contract that always reverts when receiving ETH.
|
|
719
|
-
contract RevertOnReceive {
|
|
720
|
-
receive() external payable {
|
|
721
|
-
revert("I reject ETH");
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/// @notice A split hook that always reverts on processSplitWith but can receive ERC-20 tokens.
|
|
726
|
-
contract RevertingSplitHook is IJBSplitHook {
|
|
727
|
-
function processSplitWith(JBSplitHookContext calldata) external payable override {
|
|
728
|
-
revert("I always revert");
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
|
|
732
|
-
return interfaceId == type(IJBSplitHook).interfaceId || interfaceId == type(IERC165).interfaceId;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/// @notice A simple mintable ERC20 for testing.
|
|
737
|
-
contract MintableERC20 is ERC20 {
|
|
738
|
-
uint8 internal _decimals;
|
|
739
|
-
|
|
740
|
-
constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
|
|
741
|
-
_decimals = decimals_;
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function decimals() public view override returns (uint8) {
|
|
745
|
-
return _decimals;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
function mint(address to, uint256 amount) external {
|
|
749
|
-
_mint(to, amount);
|
|
750
|
-
}
|
|
751
|
-
}
|