@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.
Files changed (77) hide show
  1. package/foundry.lock +1 -7
  2. package/foundry.toml +1 -1
  3. package/package.json +20 -9
  4. package/script/Deploy.s.sol +2 -2
  5. package/src/JB721Checkpoints.sol +60 -18
  6. package/src/JB721CheckpointsDeployer.sol +10 -5
  7. package/src/JB721TiersHook.sol +4 -1
  8. package/src/JB721TiersHookProjectDeployer.sol +68 -30
  9. package/src/JB721TiersHookStore.sol +1 -4
  10. package/src/interfaces/IJB721Checkpoints.sol +21 -14
  11. package/src/interfaces/IJB721CheckpointsDeployer.sol +6 -2
  12. package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -0
  13. package/test/utils/AccessJBLib.sol +49 -0
  14. package/test/utils/ForTest_JB721TiersHook.sol +246 -0
  15. package/test/utils/TestBaseWorkflow.sol +213 -0
  16. package/test/utils/UnitTestSetup.sol +805 -0
  17. package/.gas-snapshot +0 -152
  18. package/ADMINISTRATION.md +0 -87
  19. package/ARCHITECTURE.md +0 -98
  20. package/AUDIT_INSTRUCTIONS.md +0 -77
  21. package/RISKS.md +0 -118
  22. package/SKILLS.md +0 -43
  23. package/STYLE_GUIDE.md +0 -610
  24. package/USER_JOURNEYS.md +0 -121
  25. package/assets/findings/nana-721-hook-v6-pashov-ai-audit-report-20260330-091257.md +0 -83
  26. package/slither-ci.config.json +0 -10
  27. package/test/721HookAttacks.t.sol +0 -408
  28. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +0 -985
  29. package/test/Fork.t.sol +0 -2346
  30. package/test/TestAuditGaps.sol +0 -1075
  31. package/test/TestCheckpoints.t.sol +0 -341
  32. package/test/TestSafeTransferReentrancy.t.sol +0 -305
  33. package/test/TestVotingUnitsLifecycle.t.sol +0 -313
  34. package/test/audit/AuditRegressions.t.sol +0 -83
  35. package/test/audit/CrossCurrencySplitNoPrices.t.sol +0 -123
  36. package/test/audit/FreshAudit.t.sol +0 -197
  37. package/test/audit/FutureTierPoC.t.sol +0 -39
  38. package/test/audit/FutureTierRemoval.t.sol +0 -47
  39. package/test/audit/Pass12L18.t.sol +0 -80
  40. package/test/audit/PayCreditsBypassTierSplits.t.sol +0 -200
  41. package/test/audit/ProjectDeployerAuth.t.sol +0 -266
  42. package/test/audit/RepoFindings.t.sol +0 -195
  43. package/test/audit/ReserveActivation.t.sol +0 -87
  44. package/test/audit/RetroactiveReserveBeneficiaryDilution.t.sol +0 -149
  45. package/test/audit/SameCurrencyDecimalMismatch.t.sol +0 -249
  46. package/test/audit/SplitCreditsMismatch.t.sol +0 -219
  47. package/test/audit/SplitFailureRedistribution.t.sol +0 -143
  48. package/test/audit/USDTVoidReturnCompat.t.sol +0 -301
  49. package/test/fork/ERC20CashOutFork.t.sol +0 -633
  50. package/test/fork/ERC20TierSplitFork.t.sol +0 -596
  51. package/test/fork/IssueTokensForSplitsFork.t.sol +0 -516
  52. package/test/invariants/TierLifecycleInvariant.t.sol +0 -188
  53. package/test/invariants/TieredHookStoreInvariant.t.sol +0 -86
  54. package/test/invariants/handlers/TierLifecycleHandler.sol +0 -300
  55. package/test/invariants/handlers/TierStoreHandler.sol +0 -165
  56. package/test/regression/BrokenTerminalDoesNotDos.t.sol +0 -277
  57. package/test/regression/CacheTierLookup.t.sol +0 -190
  58. package/test/regression/ProjectDeployerRulesets.t.sol +0 -358
  59. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +0 -155
  60. package/test/regression/SplitDistributionBugs.t.sol +0 -751
  61. package/test/regression/SplitNoBeneficiary.t.sol +0 -140
  62. package/test/unit/AuditFixes_Unit.t.sol +0 -624
  63. package/test/unit/JB721CheckpointsDeployer_AccessControl.t.sol +0 -116
  64. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +0 -144
  65. package/test/unit/JBBitmap.t.sol +0 -170
  66. package/test/unit/JBIpfsDecoder.t.sol +0 -136
  67. package/test/unit/TierSupplyReserveCheck.t.sol +0 -221
  68. package/test/unit/adjustTier_Unit.t.sol +0 -1942
  69. package/test/unit/deployer_Unit.t.sol +0 -114
  70. package/test/unit/getters_constructor_Unit.t.sol +0 -593
  71. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +0 -452
  72. package/test/unit/pay_CrossCurrency_Unit.t.sol +0 -530
  73. package/test/unit/pay_Unit.t.sol +0 -1661
  74. package/test/unit/redeem_Unit.t.sol +0 -473
  75. package/test/unit/relayBeneficiary_Unit.t.sol +0 -182
  76. package/test/unit/splitHookDistribution_Unit.t.sol +0 -604
  77. package/test/unit/tierSplitRouting_Unit.t.sol +0 -757
@@ -1,165 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- import {CommonBase} from "forge-std/Base.sol";
5
- import {StdCheats} from "forge-std/StdCheats.sol";
6
- import {StdUtils} from "forge-std/StdUtils.sol";
7
-
8
- import {JB721TiersHookStore} from "../../../src/JB721TiersHookStore.sol";
9
- import {JB721TierConfig} from "../../../src/structs/JB721TierConfig.sol";
10
- import {JB721TierConfigFlags} from "../../../src/structs/JB721TierConfigFlags.sol";
11
- // forge-lint: disable-next-line(unused-import)
12
- import {JB721TiersHookFlags} from "../../../src/structs/JB721TiersHookFlags.sol";
13
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
14
-
15
- /// @notice Handler for JB721TiersHookStore invariant tests.
16
- /// @dev Acts as the "hook" address itself, so msg.sender (this) == hook in the store.
17
- contract TierStoreHandler is CommonBase, StdCheats, StdUtils {
18
- JB721TiersHookStore public immutable STORE;
19
-
20
- // This contract acts as the hook.
21
- // forge-lint: disable-next-line(mixed-case-variable)
22
- address public HOOK;
23
-
24
- // Ghost variable tracking the max tier ID seen.
25
- uint256 public lowestMaxTierIdSeen;
26
-
27
- // Track how many tiers we've added.
28
- uint256 public tiersAdded;
29
-
30
- // Track minted token IDs so we can only burn minted tokens.
31
- uint256[] public mintedTokenIds;
32
-
33
- // Track burned token IDs to avoid double-burn.
34
- mapping(uint256 => bool) public wasBurned;
35
-
36
- constructor(JB721TiersHookStore store) {
37
- STORE = store;
38
- HOOK = address(this);
39
- }
40
-
41
- /// @notice Add a new tier.
42
- function addTier(uint104 price, uint32 initialSupply, uint16 reserveFrequency, uint24 category) public {
43
- // Bound inputs to valid ranges.
44
- initialSupply = uint32(bound(initialSupply, 1, 1_000_000));
45
- price = uint104(bound(price, 1, type(uint104).max));
46
- category = uint24(bound(category, 0, 100));
47
-
48
- // Reserve frequency must be <= initialSupply if non-zero.
49
- if (reserveFrequency > 0) {
50
- reserveFrequency = uint16(bound(reserveFrequency, 1, 200));
51
- }
52
-
53
- JB721TierConfig[] memory configs = new JB721TierConfig[](1);
54
- configs[0] = JB721TierConfig({
55
- price: price,
56
- initialSupply: initialSupply,
57
- votingUnits: 0,
58
- reserveFrequency: reserveFrequency,
59
- reserveBeneficiary: address(0),
60
- encodedIPFSUri: bytes32(0),
61
- category: category,
62
- discountPercent: 0,
63
- flags: JB721TierConfigFlags({
64
- allowOwnerMint: true,
65
- useReserveBeneficiaryAsDefault: false,
66
- transfersPausable: false,
67
- useVotingUnits: false,
68
- cantBeRemoved: false,
69
- cantIncreaseDiscountPercent: false,
70
- cantBuyWithCredits: false
71
- }),
72
- splitPercent: 0,
73
- splits: new JBSplit[](0)
74
- });
75
-
76
- try this._doAddTiers(configs) {
77
- tiersAdded++;
78
- _updateMaxTierIdSeen();
79
- } catch {}
80
- }
81
-
82
- /// @dev External wrapper so calldata encoding is correct for the store.
83
- function _doAddTiers(JB721TierConfig[] calldata configs) external {
84
- STORE.recordAddTiers(configs);
85
- }
86
-
87
- /// @notice Remove a tier.
88
- function removeTier(uint256 tierId) public {
89
- uint256 maxId = STORE.maxTierIdOf(HOOK);
90
- if (maxId == 0) return;
91
-
92
- tierId = bound(tierId, 1, maxId);
93
-
94
- uint256[] memory ids = new uint256[](1);
95
- ids[0] = tierId;
96
-
97
- try this._doRemoveTiers(ids) {} catch {}
98
- }
99
-
100
- /// @dev External wrapper for calldata.
101
- function _doRemoveTiers(uint256[] calldata ids) external {
102
- STORE.recordRemoveTierIds(ids);
103
- }
104
-
105
- /// @notice Mint from a tier (simulates store recording a mint).
106
- function mint(uint256 tierId) public {
107
- uint256 maxId = STORE.maxTierIdOf(HOOK);
108
- if (maxId == 0) return;
109
-
110
- tierId = bound(tierId, 1, maxId);
111
-
112
- uint16[] memory tierIds = new uint16[](1);
113
- // forge-lint: disable-next-line(unsafe-typecast)
114
- tierIds[0] = uint16(tierId);
115
-
116
- try this._doMint(type(uint256).max, tierIds) returns (uint256[] memory tokenIds) {
117
- for (uint256 i; i < tokenIds.length; i++) {
118
- if (tokenIds[i] != 0) {
119
- mintedTokenIds.push(tokenIds[i]);
120
- }
121
- }
122
- } catch {}
123
-
124
- _updateMaxTierIdSeen();
125
- }
126
-
127
- /// @dev External wrapper for calldata.
128
- function _doMint(uint256 amount, uint16[] calldata tierIds) external returns (uint256[] memory tokenIds) {
129
- (tokenIds,,) = STORE.recordMint(amount, tierIds, true);
130
- }
131
-
132
- /// @notice Burn a minted token.
133
- function burn(uint256 indexSeed) public {
134
- if (mintedTokenIds.length == 0) return;
135
-
136
- uint256 index = bound(indexSeed, 0, mintedTokenIds.length - 1);
137
- uint256 tokenId = mintedTokenIds[index];
138
-
139
- // Skip if already burned.
140
- if (wasBurned[tokenId]) return;
141
-
142
- uint256[] memory tokenIds = new uint256[](1);
143
- tokenIds[0] = tokenId;
144
-
145
- try this._doBurn(tokenIds) {
146
- wasBurned[tokenId] = true;
147
- } catch {}
148
- }
149
-
150
- /// @dev External wrapper for calldata.
151
- function _doBurn(uint256[] calldata tokenIds) external {
152
- STORE.recordBurn(tokenIds);
153
- }
154
-
155
- function mintedTokenCount() external view returns (uint256) {
156
- return mintedTokenIds.length;
157
- }
158
-
159
- function _updateMaxTierIdSeen() internal {
160
- uint256 current = STORE.maxTierIdOf(HOOK);
161
- if (lowestMaxTierIdSeen == 0 || current > lowestMaxTierIdSeen) {
162
- lowestMaxTierIdSeen = current;
163
- }
164
- }
165
- }
@@ -1,277 +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
- // forge-lint: disable-next-line(unused-import)
8
- import {JB721TiersHookLib} from "../../src/libraries/JB721TiersHookLib.sol";
9
- import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
10
- import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
11
- import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
12
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
13
- import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
14
- import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
15
-
16
- /// @notice Regression tests: a broken project terminal in _addToBalance now reverts the payment (M-4 fix).
17
- contract Test_BrokenTerminalDoesNotDos is UnitTestSetup {
18
- using stdStorage for StdStorage;
19
-
20
- function setUp() public override {
21
- super.setUp();
22
- vm.etch(mockJBSplits, new bytes(0x69));
23
- }
24
-
25
- // Helper: build payer metadata for tier IDs.
26
- function _buildPayerMetadata(
27
- address hookAddress,
28
- uint16[] memory tierIdsToMint
29
- )
30
- internal
31
- view
32
- returns (bytes memory)
33
- {
34
- bytes[] memory data = new bytes[](1);
35
- data[0] = abi.encode(false, tierIdsToMint);
36
- bytes4[] memory ids = new bytes4[](1);
37
- ids[0] = metadataHelper.getId("pay", hookAddress);
38
- return metadataHelper.createMetadata(ids, data);
39
- }
40
-
41
- /// @notice Helper to build an ERC-20 afterPay context (reduces stack depth).
42
- function _buildErc20PayContext(
43
- address hookAddress,
44
- address token,
45
- uint32 currency,
46
- uint8 decimals,
47
- uint256[] memory tierIds,
48
- uint256 amount
49
- )
50
- internal
51
- view
52
- returns (JBAfterPayRecordedContext memory)
53
- {
54
- uint16[] memory mintIds = new uint16[](1);
55
- mintIds[0] = uint16(tierIds[0]);
56
- bytes memory payerMetadata = _buildPayerMetadata(hookAddress, mintIds);
57
-
58
- uint16[] memory splitTierIds = new uint16[](1);
59
- splitTierIds[0] = uint16(tierIds[0]);
60
- uint256[] memory splitAmounts = new uint256[](1);
61
- splitAmounts[0] = amount;
62
-
63
- return JBAfterPayRecordedContext({
64
- payer: beneficiary,
65
- projectId: projectId,
66
- rulesetId: 0,
67
- amount: JBTokenAmount({token: token, value: amount, decimals: decimals, currency: currency}),
68
- forwardedAmount: JBTokenAmount({token: token, value: amount, decimals: decimals, currency: currency}),
69
- weight: 10e18,
70
- newlyIssuedTokenCount: 0,
71
- beneficiary: beneficiary,
72
- hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
73
- payerMetadata: payerMetadata
74
- });
75
- }
76
-
77
- // ──────────────────────────────────────────────────────────────────────
78
- // ETH: broken own-project terminal in _addToBalance should revert (M-4)
79
- // ──────────────────────────────────────────────────────────────────────
80
-
81
- /// @notice When a split has no valid recipient (projectId==0, beneficiary==address(0)),
82
- /// funds route to the project's own terminal via _addToBalance. If that terminal reverts,
83
- /// the payment now reverts with JB721TiersHookLib_SplitFallbackFailed (M-4 fix).
84
- function test_brokenOwnTerminal_eth_reverts() public {
85
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
86
- IJB721TiersHookStore hookStore = testHook.STORE();
87
-
88
- // Add a tier with 100% split, priced at 1 ETH.
89
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
90
- tierConfigs[0].price = 1 ether;
91
- tierConfigs[0].initialSupply = uint32(100);
92
- tierConfigs[0].category = uint24(1);
93
- tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
94
- tierConfigs[0].splitPercent = 1_000_000_000; // 100%
95
-
96
- vm.prank(address(testHook));
97
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
98
-
99
- // Mock directory checks.
100
- mockAndExpect(
101
- address(mockJBDirectory),
102
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
103
- abi.encode(true)
104
- );
105
-
106
- // Mock splits: a split with no valid recipient (projectId==0, beneficiary==address(0)),
107
- // so funds route to _addToBalance.
108
- JBSplit[] memory splits = new JBSplit[](1);
109
- splits[0] = JBSplit({
110
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
111
- projectId: 0,
112
- beneficiary: payable(address(0)),
113
- preferAddToBalance: false,
114
- lockedUntil: 0,
115
- hook: IJBSplitHook(address(0))
116
- });
117
-
118
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
119
- mockAndExpect(
120
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
121
- );
122
-
123
- // Mock the project's primary terminal to a contract that reverts on addToBalanceOf.
124
- address brokenTerminal = makeAddr("brokenTerminal");
125
- vm.etch(brokenTerminal, new bytes(0x69));
126
- mockAndExpect(
127
- address(mockJBDirectory),
128
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, JBConstants.NATIVE_TOKEN),
129
- abi.encode(brokenTerminal)
130
- );
131
- // Make addToBalanceOf revert.
132
- vm.mockCallRevert(
133
- brokenTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), "terminal broken"
134
- );
135
-
136
- // Build payer metadata.
137
- uint16[] memory mintIds = new uint16[](1);
138
- mintIds[0] = uint16(tierIds[0]);
139
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
140
-
141
- // Build hook metadata (per-tier split breakdown).
142
- uint16[] memory splitTierIds = new uint16[](1);
143
- splitTierIds[0] = uint16(tierIds[0]);
144
- uint256[] memory splitAmounts = new uint256[](1);
145
- splitAmounts[0] = 1 ether;
146
-
147
- JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
148
- payer: beneficiary,
149
- projectId: projectId,
150
- rulesetId: 0,
151
- amount: JBTokenAmount({
152
- token: JBConstants.NATIVE_TOKEN,
153
- value: 1 ether,
154
- decimals: 18,
155
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
156
- }),
157
- forwardedAmount: JBTokenAmount({
158
- token: JBConstants.NATIVE_TOKEN,
159
- value: 1 ether,
160
- decimals: 18,
161
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
162
- }),
163
- weight: 10e18,
164
- newlyIssuedTokenCount: 0,
165
- beneficiary: beneficiary,
166
- hookMetadata: abi.encode(beneficiary, beneficiary, abi.encode(splitTierIds, splitAmounts)),
167
- payerMetadata: payerMetadata
168
- });
169
-
170
- vm.deal(mockTerminalAddress, 2 ether);
171
-
172
- vm.prank(mockTerminalAddress);
173
- // The payment should now revert when the fallback addToBalanceOf call fails (M-4).
174
- vm.expectRevert();
175
- testHook.afterPayRecordedWith{value: 1 ether}(payContext);
176
- }
177
-
178
- // ──────────────────────────────────────────────────────────────────────
179
- // ERC-20: broken own-project terminal in _addToBalance should revert (M-4)
180
- // ──────────────────────────────────────────────────────────────────────
181
-
182
- /// @notice Same scenario as above but with ERC-20 tokens. On terminal failure, the
183
- /// approval is reset to 0 for safety and the payment now reverts (M-4 fix).
184
- function test_brokenOwnTerminal_erc20_reverts() public {
185
- BrokenTerminalERC20 usdc = new BrokenTerminalERC20("USD Coin", "USDC", 6);
186
- uint32 usdcCurrency = uint32(uint160(address(usdc)));
187
-
188
- JB721TiersHook testHook = _initHookDefaultTiers(0, false, usdcCurrency, 6);
189
-
190
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
191
- tierConfigs[0].price = 100e6;
192
- tierConfigs[0].initialSupply = uint32(100);
193
- tierConfigs[0].category = uint24(1);
194
- tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
195
- tierConfigs[0].splitPercent = 1_000_000_000; // 100%
196
-
197
- vm.prank(address(testHook));
198
- uint256[] memory tierIds = testHook.STORE().recordAddTiers(tierConfigs);
199
-
200
- _setupErc20Mocks(testHook, tierIds, address(usdc), usdcCurrency);
201
-
202
- JBAfterPayRecordedContext memory payContext =
203
- _buildErc20PayContext(address(testHook), address(usdc), usdcCurrency, 6, tierIds, 100e6);
204
-
205
- // Fund the terminal and approve the hook to pull tokens.
206
- usdc.mint(mockTerminalAddress, 100e6);
207
- vm.prank(mockTerminalAddress);
208
- usdc.approve(address(testHook), 100e6);
209
-
210
- vm.prank(mockTerminalAddress);
211
- // The payment should now revert when the fallback addToBalanceOf call fails (M-4).
212
- vm.expectRevert();
213
- testHook.afterPayRecordedWith(payContext);
214
- }
215
-
216
- /// @notice Sets up mocks for the ERC-20 broken terminal test (extracted to avoid stack-too-deep).
217
- function _setupErc20Mocks(
218
- JB721TiersHook testHook,
219
- uint256[] memory tierIds,
220
- address token,
221
- uint32 /* usdcCurrency */
222
- )
223
- internal
224
- {
225
- mockAndExpect(
226
- address(mockJBDirectory),
227
- abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
228
- abi.encode(true)
229
- );
230
-
231
- // Mock splits: no valid recipient, so funds route to _addToBalance.
232
- JBSplit[] memory splits = new JBSplit[](1);
233
- splits[0] = JBSplit({
234
- percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
235
- projectId: 0,
236
- beneficiary: payable(address(0)),
237
- preferAddToBalance: false,
238
- lockedUntil: 0,
239
- hook: IJBSplitHook(address(0))
240
- });
241
-
242
- uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
243
- mockAndExpect(
244
- mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
245
- );
246
-
247
- // Mock the project's primary terminal to a contract that reverts on addToBalanceOf.
248
- address brokenTerminal = makeAddr("brokenTerminal");
249
- vm.etch(brokenTerminal, new bytes(0x69));
250
- mockAndExpect(
251
- address(mockJBDirectory),
252
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, projectId, token),
253
- abi.encode(brokenTerminal)
254
- );
255
- // Make addToBalanceOf revert.
256
- vm.mockCallRevert(
257
- brokenTerminal, abi.encodeWithSelector(IJBTerminal.addToBalanceOf.selector), "terminal broken"
258
- );
259
- }
260
- }
261
-
262
- /// @notice A simple mintable ERC20 for testing.
263
- contract BrokenTerminalERC20 is ERC20 {
264
- uint8 internal _decimals;
265
-
266
- constructor(string memory name_, string memory symbol_, uint8 decimals_) ERC20(name_, symbol_) {
267
- _decimals = decimals_;
268
- }
269
-
270
- function decimals() public view override returns (uint8) {
271
- return _decimals;
272
- }
273
-
274
- function mint(address to, uint256 amount) external {
275
- _mint(to, amount);
276
- }
277
- }
@@ -1,190 +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
- // forge-lint: disable-next-line(unused-import)
8
- import {JB721TiersHookLib} from "../../src/libraries/JB721TiersHookLib.sol";
9
-
10
- /// @notice calculateSplitAmounts caches the tierOf result to avoid a duplicate external call.
11
- /// Verifies that the cached tier lookup returns the same split amounts as reading price and splitPercent individually.
12
- contract Test_L35_CacheTierLookup is UnitTestSetup {
13
- using stdStorage for StdStorage;
14
-
15
- /// @notice Verify that calculateSplitAmounts returns correct per-tier amounts when multiple tiers have different
16
- /// prices and split percentages. This exercises the cached `tier` variable.
17
- function test_calculateSplitAmounts_multiTier_correctAmounts() public {
18
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
19
- IJB721TiersHookStore hookStore = testHook.STORE();
20
-
21
- // Add 3 tiers with different prices and split percents.
22
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](3);
23
-
24
- // Tier A: 1 ETH, 25% split -> 0.25 ETH
25
- tierConfigs[0].price = 1 ether;
26
- tierConfigs[0].initialSupply = uint32(100);
27
- tierConfigs[0].category = uint24(1);
28
- tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1111));
29
- tierConfigs[0].splitPercent = 250_000_000; // 25%
30
-
31
- // Tier B: 2 ETH, 50% split -> 1 ETH
32
- tierConfigs[1].price = 2 ether;
33
- tierConfigs[1].initialSupply = uint32(100);
34
- tierConfigs[1].category = uint24(2);
35
- tierConfigs[1].encodedIPFSUri = bytes32(uint256(0x2222));
36
- tierConfigs[1].splitPercent = 500_000_000; // 50%
37
-
38
- // Tier C: 0.5 ETH, 100% split -> 0.5 ETH
39
- tierConfigs[2].price = 0.5 ether;
40
- tierConfigs[2].initialSupply = uint32(100);
41
- tierConfigs[2].category = uint24(3);
42
- tierConfigs[2].encodedIPFSUri = bytes32(uint256(0x3333));
43
- tierConfigs[2].splitPercent = 1_000_000_000; // 100%
44
-
45
- vm.prank(address(testHook));
46
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
47
-
48
- // Build payer metadata requesting all 3 tiers.
49
- uint16[] memory mintIds = new uint16[](3);
50
- mintIds[0] = uint16(tierIds[0]);
51
- mintIds[1] = uint16(tierIds[1]);
52
- mintIds[2] = uint16(tierIds[2]);
53
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
54
-
55
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
56
- terminal: mockTerminalAddress,
57
- payer: beneficiary,
58
- amount: JBTokenAmount({
59
- token: JBConstants.NATIVE_TOKEN,
60
- value: 3.5 ether,
61
- decimals: 18,
62
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
63
- }),
64
- projectId: projectId,
65
- rulesetId: 0,
66
- beneficiary: beneficiary,
67
- weight: 10e18,
68
- reservedPercent: 5000,
69
- metadata: payerMetadata
70
- });
71
-
72
- (, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
73
-
74
- // Total split = 0.25 + 1.0 + 0.5 = 1.75 ETH
75
- assertEq(specs[0].amount, 1.75 ether, "Total split amount should be 1.75 ETH");
76
- }
77
-
78
- /// @notice Verify that a tier with splitPercent == 0 contributes nothing to the total, even when cached.
79
- function test_calculateSplitAmounts_zeroSplitSkipped() public {
80
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
81
- IJB721TiersHookStore hookStore = testHook.STORE();
82
-
83
- // Tier A: 1 ETH, 50% split
84
- // Tier B: 3 ETH, 0% split (should be skipped)
85
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](2);
86
- tierConfigs[0].price = 1 ether;
87
- tierConfigs[0].initialSupply = uint32(100);
88
- tierConfigs[0].category = uint24(1);
89
- tierConfigs[0].encodedIPFSUri = bytes32(uint256(0xAAAA));
90
- tierConfigs[0].splitPercent = 500_000_000; // 50%
91
-
92
- tierConfigs[1].price = 3 ether;
93
- tierConfigs[1].initialSupply = uint32(100);
94
- tierConfigs[1].category = uint24(2);
95
- tierConfigs[1].encodedIPFSUri = bytes32(uint256(0xBBBB));
96
- tierConfigs[1].splitPercent = 0; // 0%
97
-
98
- vm.prank(address(testHook));
99
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
100
-
101
- uint16[] memory mintIds = new uint16[](2);
102
- mintIds[0] = uint16(tierIds[0]);
103
- mintIds[1] = uint16(tierIds[1]);
104
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
105
-
106
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
107
- terminal: mockTerminalAddress,
108
- payer: beneficiary,
109
- amount: JBTokenAmount({
110
- token: JBConstants.NATIVE_TOKEN,
111
- value: 4 ether,
112
- decimals: 18,
113
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
114
- }),
115
- projectId: projectId,
116
- rulesetId: 0,
117
- beneficiary: beneficiary,
118
- weight: 10e18,
119
- reservedPercent: 5000,
120
- metadata: payerMetadata
121
- });
122
-
123
- (, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
124
-
125
- // Only tier A contributes: 1 ETH * 50% = 0.5 ETH
126
- assertEq(specs[0].amount, 0.5 ether, "Only non-zero split tiers should contribute");
127
- }
128
-
129
- /// @notice Verify that duplicate tier IDs in metadata produce correct cumulative split amounts.
130
- /// The cached tier lookup must handle the same tier appearing multiple times.
131
- function test_calculateSplitAmounts_duplicateTierIds() public {
132
- ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
133
- IJB721TiersHookStore hookStore = testHook.STORE();
134
-
135
- // Single tier: 1 ETH, 30% split
136
- JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
137
- tierConfigs[0].price = 1 ether;
138
- tierConfigs[0].initialSupply = uint32(100);
139
- tierConfigs[0].category = uint24(1);
140
- tierConfigs[0].encodedIPFSUri = bytes32(uint256(0xCCCC));
141
- tierConfigs[0].splitPercent = 300_000_000; // 30%
142
-
143
- vm.prank(address(testHook));
144
- uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
145
-
146
- // Request the same tier twice in metadata.
147
- uint16[] memory mintIds = new uint16[](2);
148
- mintIds[0] = uint16(tierIds[0]);
149
- mintIds[1] = uint16(tierIds[0]);
150
- bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
151
-
152
- JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
153
- terminal: mockTerminalAddress,
154
- payer: beneficiary,
155
- amount: JBTokenAmount({
156
- token: JBConstants.NATIVE_TOKEN,
157
- value: 2 ether,
158
- decimals: 18,
159
- currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
160
- }),
161
- projectId: projectId,
162
- rulesetId: 0,
163
- beneficiary: beneficiary,
164
- weight: 10e18,
165
- reservedPercent: 5000,
166
- metadata: payerMetadata
167
- });
168
-
169
- (, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
170
-
171
- // 2 x (1 ETH * 30%) = 0.6 ETH
172
- assertEq(specs[0].amount, 0.6 ether, "Duplicate tier IDs should each contribute their split amount");
173
- }
174
-
175
- // Helper: build payer metadata for tier IDs.
176
- function _buildPayerMetadata(
177
- address hookAddress,
178
- uint16[] memory tierIdsToMint
179
- )
180
- internal
181
- view
182
- returns (bytes memory)
183
- {
184
- bytes[] memory data = new bytes[](1);
185
- data[0] = abi.encode(false, tierIdsToMint);
186
- bytes4[] memory ids = new bytes4[](1);
187
- ids[0] = metadataHelper.getId("pay", hookAddress);
188
- return metadataHelper.createMetadata(ids, data);
189
- }
190
- }