@bananapus/721-hook-v6 0.0.20 → 0.0.22

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 (54) hide show
  1. package/AUDIT_INSTRUCTIONS.md +1 -1
  2. package/CHANGE_LOG.md +1 -1
  3. package/README.md +1 -1
  4. package/SKILLS.md +4 -3
  5. package/STYLE_GUIDE.md +2 -2
  6. package/USER_JOURNEYS.md +0 -1
  7. package/foundry.toml +1 -1
  8. package/package.json +5 -5
  9. package/script/Deploy.s.sol +1 -1
  10. package/script/helpers/Hook721DeploymentLib.sol +1 -1
  11. package/src/JB721TiersHook.sol +11 -1
  12. package/src/JB721TiersHookDeployer.sol +1 -1
  13. package/src/JB721TiersHookProjectDeployer.sol +1 -1
  14. package/src/JB721TiersHookStore.sol +1 -1
  15. package/src/abstract/ERC721.sol +1 -1
  16. package/src/abstract/JB721Hook.sol +1 -1
  17. package/src/libraries/JB721TiersHookLib.sol +11 -6
  18. package/src/structs/JBDeploy721TiersHookConfig.sol +0 -2
  19. package/test/721HookAttacks.t.sol +1 -1
  20. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +1 -3
  21. package/test/Fork.t.sol +1 -2
  22. package/test/TestAuditGaps.sol +1 -1
  23. package/test/TestSafeTransferReentrancy.t.sol +1 -1
  24. package/test/TestVotingUnitsLifecycle.t.sol +1 -1
  25. package/test/audit/AuditRegressions.t.sol +2 -1
  26. package/test/audit/{CodexNemesis_CrossCurrencySplitNoPrices.t.sol → CrossCurrencySplitNoPrices.t.sol} +3 -2
  27. package/test/audit/SplitFailureRedistribution.t.sol +122 -0
  28. package/test/audit/USDTVoidReturnCompat.t.sol +1 -1
  29. package/test/fork/ERC20CashOutFork.t.sol +1 -2
  30. package/test/fork/ERC20TierSplitFork.t.sol +1 -3
  31. package/test/fork/IssueTokensForSplitsFork.t.sol +1 -2
  32. package/test/invariants/TierLifecycleInvariant.t.sol +1 -1
  33. package/test/invariants/TieredHookStoreInvariant.t.sol +1 -1
  34. package/test/invariants/handlers/TierLifecycleHandler.sol +1 -1
  35. package/test/invariants/handlers/TierStoreHandler.sol +1 -1
  36. package/test/regression/BrokenTerminalDoesNotDos.t.sol +1 -1
  37. package/test/regression/CacheTierLookup.t.sol +1 -1
  38. package/test/regression/ProjectDeployerRulesets.t.sol +1 -2
  39. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +1 -1
  40. package/test/regression/SplitDistributionBugs.t.sol +1 -1
  41. package/test/regression/SplitNoBeneficiary.t.sol +1 -1
  42. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +1 -1
  43. package/test/unit/JBBitmap.t.sol +1 -1
  44. package/test/unit/JBIpfsDecoder.t.sol +1 -1
  45. package/test/unit/TierSupplyReserveCheck.t.sol +1 -1
  46. package/test/unit/adjustTier_Unit.t.sol +1 -1
  47. package/test/unit/deployer_Unit.t.sol +1 -1
  48. package/test/unit/getters_constructor_Unit.t.sol +1 -2
  49. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +1 -1
  50. package/test/unit/pay_CrossCurrency_Unit.t.sol +1 -1
  51. package/test/unit/pay_Unit.t.sol +1 -1
  52. package/test/unit/redeem_Unit.t.sol +1 -1
  53. package/test/unit/splitHookDistribution_Unit.t.sol +1 -1
  54. package/test/unit/tierSplitRouting_Unit.t.sol +1 -1
@@ -8,7 +8,7 @@ Read [ARCHITECTURE.md](./ARCHITECTURE.md) first for data flow context. Read [RIS
8
8
 
9
9
  | Setting | Value |
10
10
  |---------|-------|
11
- | Solidity version | ^0.8.26 |
11
+ | Solidity version | 0.8.28 |
12
12
  | EVM target | cancun |
13
13
  | Optimizer | enabled, 200 runs |
14
14
  | via-IR | not enabled |
package/CHANGE_LOG.md CHANGED
@@ -74,7 +74,7 @@ Several store errors gained a `tierId` parameter for better debugging:
74
74
 
75
75
  ### 1.8 Solidity Version Change
76
76
 
77
- All contracts upgraded from `pragma solidity 0.8.23` to `pragma solidity ^0.8.26`.
77
+ All contracts upgraded from `pragma solidity 0.8.23` to `pragma solidity 0.8.28`.
78
78
 
79
79
  ### 1.9 Cross-Repo Impact
80
80
 
package/README.md CHANGED
@@ -146,7 +146,7 @@ Key settings from `foundry.toml`:
146
146
 
147
147
  | Setting | Value |
148
148
  | --- | --- |
149
- | Solidity compiler | ^0.8.26 |
149
+ | Solidity compiler | 0.8.28 |
150
150
  | EVM target | cancun |
151
151
  | Optimizer runs | 200 |
152
152
  | Fuzz runs | 4,096 |
package/SKILLS.md CHANGED
@@ -86,7 +86,7 @@ Tiered ERC-721 NFT hook for Juicebox V6 that mints NFTs when a project is paid a
86
86
  | `JB721Tier` | `uint32 id`, `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint104 votingUnits`, `uint16 reserveFrequency`, `address reserveBeneficiary`, `bytes32 encodedIPFSUri`, `uint24 category`, `uint8 discountPercent`, `bool allowOwnerMint`, `bool transfersPausable`, `bool cannotBeRemoved`, `bool cannotIncreaseDiscountPercent`, `uint32 splitPercent`, `string resolvedUri` | Return type from `tierOf`, `tiersOf`, `tierOfTokenId` |
87
87
  | `JBStored721Tier` | `uint104 price`, `uint32 remainingSupply`, `uint32 initialSupply`, `uint32 splitPercent`, `uint24 category`, `uint8 discountPercent`, `uint16 reserveFrequency`, `uint8 packedBools` (allowOwnerMint, transfersPausable, useVotingUnits, cannotBeRemoved, cannotIncreaseDiscountPercent) | Internal storage in `JB721TiersHookStore`. Voting units stored separately in `_tierVotingUnitsOf` when `useVotingUnits` is true. |
88
88
  | `JB721InitTiersConfig` | `JB721TierConfig[] tiers`, `uint32 currency`, `uint8 decimals` | `initialize` -- defines tiers and pricing context. The prices contract is an immutable on the hook, not passed per-config. |
89
- | `JBDeploy721TiersHookConfig` | `string name`, `string symbol`, `string baseUri`, `IJB721TokenUriResolver tokenUriResolver`, `string contractUri`, `JB721InitTiersConfig tiersConfig`, `address reserveBeneficiary`, `JB721TiersHookFlags flags` | `deployHookFor`, `launchProjectFor` |
89
+ | `JBDeploy721TiersHookConfig` | `string name`, `string symbol`, `string baseUri`, `IJB721TokenUriResolver tokenUriResolver`, `string contractUri`, `JB721InitTiersConfig tiersConfig`, `JB721TiersHookFlags flags` | `deployHookFor`, `launchProjectFor` |
90
90
  | `JB721TiersHookFlags` | `bool noNewTiersWithReserves`, `bool noNewTiersWithVotes`, `bool noNewTiersWithOwnerMinting`, `bool preventOverspending`, `bool issueTokensForSplits` | `initialize`, `recordFlags` |
91
91
  | `JB721TiersRulesetMetadata` | `bool pauseTransfers`, `bool pauseMintPendingReserves` | Packed into `JBRulesetMetadata.metadata` per-ruleset (bit 0 = pauseTransfers, bit 1 = pauseMintPendingReserves) |
92
92
  | `JBPayDataHookRulesetConfig` | `uint48 mustStartAtOrAfter`, `uint32 duration`, `uint112 weight`, `uint32 weightCutPercent`, `IJBRulesetApprovalHook approvalHook`, `JBPayDataHookRulesetMetadata metadata`, `JBSplitGroup[] splitGroups`, `JBFundAccessLimitGroup[] fundAccessLimitGroups` | `JB721TiersHookProjectDeployer` -- wraps core ruleset config with `useDataHookForPay: true` hardcoded |
@@ -145,8 +145,10 @@ Each tier has configurable voting power:
145
145
  - Computes `mulDiv(effectivePrice, splitPercent, SPLITS_TOTAL_PERCENT)` per tier.
146
146
  - Returns the total to be forwarded to the hook.
147
147
  - If the payment currency differs from the tier pricing currency, `convertSplitAmounts` converts amounts to the payment token denomination via `JBPrices`.
148
+ - **Pay credits cap**: `totalSplitAmount` is capped at `context.amount.value` before weight calculation and before being returned in the hook specification. Pay credits fund NFT minting (virtual -- they reduce the price threshold in `recordMint`), but splits require real tokens to distribute. Without this cap, a payer with sufficient pay credits but insufficient actual payment value would cause the terminal to attempt forwarding more tokens than were actually received, reverting the transaction. This means the project receiving the NFT payment must have received the full amount including what would have gone to splits -- split recipients are paid from the project's balance, not directly from the payer's contribution.
148
149
  - Weight is adjusted down proportionally (`weight = mulDiv(weight, amount - totalSplitAmount, amount)`) unless the `issueTokensForSplits` flag is set, in which case the full `context.weight` is returned.
149
150
  - In `afterPayRecordedWith`, `distributeAll` distributes forwarded funds to each tier's split group recipients. Leftover after all splits goes back to the project's balance via `addToBalance`.
151
+ - **Leftover accounting in `_distributeSingleSplit`**: `leftoverAmount` is always decremented **before** the send attempt. If `_sendPayoutToSplit` returns `false` (i.e., the send reverted or had no valid recipient), the amount is added **back** to `leftoverAmount` so it routes to the project's balance with the remaining leftover. This "decrement-first, restore-on-failure" pattern prevents a failed split from inflating subsequent split recipients' shares (since `leftoverPercentage` still decreases regardless of success).
150
152
  - Split recipients follow the same priority chain as `JBMultiTerminal`: `split.hook` > `split.projectId` > `split.beneficiary`:
151
153
  - **Split hooks**: receive a `JBSplitHookContext` struct (`token`, `amount`, `decimals`, `projectId`, `groupId`, full `split`). Native tokens forwarded via `{value: amount}`; ERC-20s transferred via `SafeERC20.safeTransfer` before calling `processSplitWith`.
152
154
  - **Project splits**: route via `terminal.pay` or `terminal.addToBalance`.
@@ -154,7 +156,7 @@ Each tier has configurable voting power:
154
156
  - **Empty splits** (no hook, no project ID, no beneficiary): skipped -- their share stays in the leftover and routes to the project's balance via `addToBalanceOf`, preventing a misconfigured split from bricking the payout distribution.
155
157
  - All external calls in `_sendPayoutToSplit` are wrapped in try-catch to prevent a single reverting recipient from bricking all payments to the project:
156
158
  - **Native token hooks**: a revert returns `false` (ETH stays in the contract and routes to the project balance).
157
- - **ERC-20 hooks**: tokens are transferred via `safeTransfer` before the callback; the function always returns `true` regardless of callback outcome because the tokens have already left -- returning `false` would cause double-spend accounting in the leftover calculation.
159
+ - **ERC-20 hooks**: tokens are transferred via `safeTransfer` before the callback; the function always returns `true` regardless of callback outcome because the tokens have already left this contract. Since leftoverAmount was already decremented before the send, returning `true` is required to prevent the caller from adding the amount back -- which would create a double-spend (tokens sent to the hook AND counted toward leftover routed to the project).
158
160
  - **ERC-20 terminal calls** (`pay`/`addToBalanceOf`): approval is reset to zero on failure to prevent dangling approvals.
159
161
 
160
162
  ## Gotchas
@@ -292,7 +294,6 @@ tiers[0] = JB721TierConfig({
292
294
  currency: 1, // ETH
293
295
  decimals: 18
294
296
  }),
295
- reserveBeneficiary: address(0),
296
297
  flags: JB721TiersHookFlags({
297
298
  noNewTiersWithReserves: false,
298
299
  noNewTiersWithVotes: false,
package/STYLE_GUIDE.md CHANGED
@@ -21,7 +21,7 @@ One contract/interface/struct/enum per file. Name the file after the type it con
21
21
 
22
22
  ```solidity
23
23
  // Contracts — pin to exact version
24
- pragma solidity ^0.8.26;
24
+ pragma solidity 0.8.28;
25
25
 
26
26
  // Interfaces, structs, enums — caret for forward compatibility
27
27
  pragma solidity ^0.8.0;
@@ -326,7 +326,7 @@ Standard config across all repos:
326
326
 
327
327
  ```toml
328
328
  [profile.default]
329
- solc = '0.8.26'
329
+ solc = '0.8.28'
330
330
  evm_version = 'cancun'
331
331
  optimizer_runs = 200
332
332
  libs = ["node_modules", "lib"]
package/USER_JOURNEYS.md CHANGED
@@ -315,7 +315,6 @@ Deploy a 721 tiers hook for an existing project.
315
315
  - `JB721TierConfig[] tiers` -- initial tiers (sorted by category).
316
316
  - `uint32 currency` -- pricing currency (`uint32(uint160(tokenAddress))` for concrete, or abstract like 1=ETH, 2=USD).
317
317
  - `uint8 decimals` -- pricing decimals (must be <= 18).
318
- - `address reserveBeneficiary` -- (unused in current initialize; set via tier configs).
319
318
  - `JB721TiersHookFlags flags` -- collection-level behavior flags.
320
319
  - `bytes32 salt` -- for deterministic deployment (bytes32(0) for non-deterministic).
321
320
 
package/foundry.toml CHANGED
@@ -1,5 +1,5 @@
1
1
  [profile.default]
2
- solc = '0.8.26'
2
+ solc = '0.8.28'
3
3
  evm_version = 'cancun'
4
4
  optimizer_runs = 200
5
5
  libs = ["node_modules", "lib"]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.20",
3
+ "version": "0.0.22",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,10 +17,10 @@
17
17
  "artifacts": "source ./.env && npx sphinx artifacts --org-id 'ea165b21-7cdc-4d7b-be59-ecdd4c26bee4' --project-name 'nana-721-hook-v6'"
18
18
  },
19
19
  "dependencies": {
20
- "@bananapus/address-registry-v6": "^0.0.14",
21
- "@bananapus/core-v6": "^0.0.26",
22
- "@bananapus/ownable-v6": "^0.0.13",
23
- "@bananapus/permission-ids-v6": "^0.0.12",
20
+ "@bananapus/address-registry-v6": "^0.0.16",
21
+ "@bananapus/core-v6": "^0.0.28",
22
+ "@bananapus/ownable-v6": "^0.0.15",
23
+ "@bananapus/permission-ids-v6": "^0.0.14",
24
24
  "@openzeppelin/contracts": "^5.6.1",
25
25
  "@prb/math": "^4.1.0",
26
26
  "solady": "^0.1.8"
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {stdJson} from "forge-std/Script.sol";
5
5
  import {Vm} from "forge-std/Vm.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
5
5
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
@@ -202,6 +202,16 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
202
202
  });
203
203
  }
204
204
 
205
+ // Cap the split amount at the actual payment value. Pay credits fund NFT minting (virtual), but splits
206
+ // require real tokens to distribute. Without this cap, a user with sufficient pay credits but insufficient
207
+ // ETH would revert because the terminal can't forward more than what was actually paid.
208
+ // This requires the project receiving the NFT payment to have received the full amount including what
209
+ // would have gone to splits — the split recipients get paid from the project's balance, not directly
210
+ // from the payer's contribution.
211
+ if (totalSplitAmount > context.amount.value) {
212
+ totalSplitAmount = context.amount.value;
213
+ }
214
+
205
215
  // Adjust weight so the terminal mints tokens only for the amount that actually enters the project.
206
216
  weight = JB721TiersHookLib.calculateWeight({
207
217
  contextWeight: context.weight,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
5
5
  import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
5
5
  import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {mulDiv} from "@prb/math/src/Common.sol";
5
5
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  // OpenZeppelin Contracts (last updated v5.0.0) (token/ERC721/ERC721.sol)
3
3
 
4
- pragma solidity ^0.8.26;
4
+ pragma solidity 0.8.28;
5
5
 
6
6
  import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
7
7
  import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
5
5
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
5
5
  import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
@@ -400,10 +400,13 @@ library JB721TiersHookLib {
400
400
  uint256 payoutAmount =
401
401
  mulDiv({x: leftoverAmount, y: tierSplits[j].percent, denominator: leftoverPercentage});
402
402
  if (payoutAmount != 0) {
403
- // Only subtract from leftover if the split has a valid recipient.
404
- // Splits with no projectId and no beneficiary are skipped their share
405
- // stays in leftoverAmount and is added to the project's balance below.
406
- if (_sendPayoutToSplit({
403
+ // Always subtract from leftover to prevent failed splits from inflating later recipients'
404
+ // shares. Failed amounts stay in the contract and are routed to the project's balance with
405
+ // the leftover below.
406
+ unchecked {
407
+ leftoverAmount -= payoutAmount;
408
+ }
409
+ if (!_sendPayoutToSplit({
407
410
  directory: directory,
408
411
  split: tierSplits[j],
409
412
  token: token,
@@ -412,8 +415,10 @@ library JB721TiersHookLib {
412
415
  groupId: groupId,
413
416
  decimals: decimals
414
417
  })) {
418
+ // The payout failed — the funds are still in this contract. Add back to leftover so they
419
+ // route to the project's balance after the loop.
415
420
  unchecked {
416
- leftoverAmount -= payoutAmount;
421
+ leftoverAmount += payoutAmount;
417
422
  }
418
423
  }
419
424
  }
@@ -11,7 +11,6 @@ import {IJB721TokenUriResolver} from "../interfaces/IJB721TokenUriResolver.sol";
11
11
  /// @custom:member tokenUriResolver The contract responsible for resolving the URI for each NFT.
12
12
  /// @custom:member contractUri The URI where this contract's metadata can be found.
13
13
  /// @custom:member tiersConfig The NFT tiers and pricing config to launch the hook with.
14
- /// @custom:member reserveBeneficiary The default reserved beneficiary for all tiers.
15
14
  /// @custom:member flags A set of boolean options to configure the hook with.
16
15
  // forge-lint: disable-next-line(pascal-case-struct)
17
16
  struct JBDeploy721TiersHookConfig {
@@ -21,6 +20,5 @@ struct JBDeploy721TiersHookConfig {
21
20
  IJB721TokenUriResolver tokenUriResolver;
22
21
  string contractUri;
23
22
  JB721InitTiersConfig tiersConfig;
24
- address reserveBeneficiary;
25
23
  JB721TiersHookFlags flags;
26
24
  }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
@@ -815,7 +815,6 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
815
815
  tiersConfig: JB721InitTiersConfig({
816
816
  tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
817
817
  }),
818
- reserveBeneficiary: reserveBeneficiary,
819
818
  flags: JB721TiersHookFlags({
820
819
  preventOverspending: false,
821
820
  issueTokensForSplits: false,
@@ -908,7 +907,6 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
908
907
  tiersConfig: JB721InitTiersConfig({
909
908
  tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
910
909
  }),
911
- reserveBeneficiary: reserveBeneficiary,
912
910
  flags: JB721TiersHookFlags({
913
911
  preventOverspending: false,
914
912
  issueTokensForSplits: false,
package/test/Fork.t.sol CHANGED
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -272,7 +272,6 @@ contract Fork_721Hook_Test is Test {
272
272
  currency: uint32(uint160(NATIVE_TOKEN)),
273
273
  decimals: 18
274
274
  }),
275
- reserveBeneficiary: reserveBeneficiary,
276
275
  flags: flags
277
276
  });
278
277
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./utils/UnitTestSetup.sol";
@@ -1,7 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // Import the shared unit test setup which deploys a hook clone with 10 tiers.
5
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
6
  import "../utils/UnitTestSetup.sol";
6
7
 
7
8
  // Import IERC2981 to compute its interface ID for the supportsInterface test.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "../utils/UnitTestSetup.sol";
5
6
  import {IJB721TokenUriResolver} from "../../src/interfaces/IJB721TokenUriResolver.sol";
6
7
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
@@ -10,7 +11,7 @@ import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
10
11
  import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
11
12
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
12
13
 
13
- contract CodexNemesis_CrossCurrencySplitNoPrices is UnitTestSetup {
14
+ contract CrossCurrencySplitNoPrices is UnitTestSetup {
14
15
  function test_crossCurrencySplit_withoutPrices_locksForwardedNativeFunds() public {
15
16
  JB721TiersHook noPricesOrigin = new JB721TiersHook(
16
17
  IJBDirectory(mockJBDirectory),
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "../utils/UnitTestSetup.sol";
6
+ import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
7
+ import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
8
+ import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
9
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
10
+
11
+ contract SplitFailureRedistribution is UnitTestSetup {
12
+ address internal alice = makeAddr("alice");
13
+ address internal bob = makeAddr("bob");
14
+
15
+ function test_failedEarlierSplit_overpaysLaterSplit() public {
16
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
17
+ IJB721TiersHookStore hookStore = testHook.STORE();
18
+
19
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
20
+ tierConfigs[0].price = 1 ether;
21
+ tierConfigs[0].initialSupply = uint32(100);
22
+ tierConfigs[0].category = uint24(1);
23
+ tierConfigs[0].encodedIPFSUri = bytes32(uint256(0x1234));
24
+ tierConfigs[0].splitPercent = 1_000_000_000;
25
+
26
+ vm.prank(address(testHook));
27
+ uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
28
+
29
+ mockAndExpect(
30
+ address(mockJBDirectory),
31
+ abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
32
+ abi.encode(true)
33
+ );
34
+
35
+ RevertOnReceive revertingBeneficiary = new RevertOnReceive();
36
+
37
+ JBSplit[] memory splits = new JBSplit[](2);
38
+ splits[0] = JBSplit({
39
+ percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2),
40
+ projectId: 0,
41
+ beneficiary: payable(address(revertingBeneficiary)),
42
+ preferAddToBalance: false,
43
+ lockedUntil: 0,
44
+ hook: IJBSplitHook(address(0))
45
+ });
46
+ splits[1] = JBSplit({
47
+ percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT / 2),
48
+ projectId: 0,
49
+ beneficiary: payable(bob),
50
+ preferAddToBalance: false,
51
+ lockedUntil: 0,
52
+ hook: IJBSplitHook(address(0))
53
+ });
54
+
55
+ uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
56
+ mockAndExpect(
57
+ mockJBSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
58
+ );
59
+
60
+ bytes memory payerMetadata = _buildPayMetadata(address(testHook), uint16(tierIds[0]));
61
+ bytes memory hookMetadata = abi.encode(_singleTierId(uint16(tierIds[0])), _singleAmount(1 ether));
62
+
63
+ JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
64
+ payer: beneficiary,
65
+ projectId: projectId,
66
+ rulesetId: 0,
67
+ amount: JBTokenAmount({
68
+ token: JBConstants.NATIVE_TOKEN,
69
+ value: 1 ether,
70
+ decimals: 18,
71
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
72
+ }),
73
+ forwardedAmount: JBTokenAmount({
74
+ token: JBConstants.NATIVE_TOKEN,
75
+ value: 1 ether,
76
+ decimals: 18,
77
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
78
+ }),
79
+ weight: 10e18,
80
+ newlyIssuedTokenCount: 0,
81
+ beneficiary: beneficiary,
82
+ hookMetadata: hookMetadata,
83
+ payerMetadata: payerMetadata
84
+ });
85
+
86
+ uint256 bobBalanceBefore = bob.balance;
87
+
88
+ vm.deal(mockTerminalAddress, 1 ether);
89
+ vm.prank(mockTerminalAddress);
90
+ testHook.afterPayRecordedWith{value: 1 ether}(payContext);
91
+
92
+ assertEq(
93
+ bob.balance - bobBalanceBefore,
94
+ 1 ether,
95
+ "later split receives the failed split's share instead of only its own allocation"
96
+ );
97
+ }
98
+
99
+ function _buildPayMetadata(address hookAddress, uint16 tierId) internal view returns (bytes memory) {
100
+ bytes[] memory data = new bytes[](1);
101
+ data[0] = abi.encode(false, _singleTierId(tierId));
102
+ bytes4[] memory ids = new bytes4[](1);
103
+ ids[0] = metadataHelper.getId("pay", hookAddress);
104
+ return metadataHelper.createMetadata(ids, data);
105
+ }
106
+
107
+ function _singleTierId(uint16 tierId) internal pure returns (uint16[] memory tierIds) {
108
+ tierIds = new uint16[](1);
109
+ tierIds[0] = tierId;
110
+ }
111
+
112
+ function _singleAmount(uint256 amount) internal pure returns (uint256[] memory amounts) {
113
+ amounts = new uint256[](1);
114
+ amounts[0] = amount;
115
+ }
116
+ }
117
+
118
+ contract RevertOnReceive {
119
+ receive() external payable {
120
+ revert("NO_ETH");
121
+ }
122
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // Import the forge-std test framework.
5
5
  import {Test} from "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -249,7 +249,6 @@ contract ERC20CashOutFork is Test {
249
249
  tokenUriResolver: IJB721TokenUriResolver(address(0)),
250
250
  contractUri: "ipfs://contract",
251
251
  tiersConfig: JB721InitTiersConfig({tiers: tierConfigs, currency: currency, decimals: 6}),
252
- reserveBeneficiary: reserveBeneficiary,
253
252
  flags: JB721TiersHookFlags({
254
253
  preventOverspending: false,
255
254
  issueTokensForSplits: false,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -226,7 +226,6 @@ contract ERC20TierSplitFork is Test {
226
226
  tokenUriResolver: IJB721TokenUriResolver(address(0)),
227
227
  contractUri: "ipfs://contract",
228
228
  tiersConfig: JB721InitTiersConfig({tiers: tierConfigs, currency: currency, decimals: tokenDecimals}),
229
- reserveBeneficiary: reserveBeneficiary,
230
229
  flags: JB721TiersHookFlags({
231
230
  preventOverspending: false,
232
231
  issueTokensForSplits: false,
@@ -300,7 +299,6 @@ contract ERC20TierSplitFork is Test {
300
299
  tiersConfig: JB721InitTiersConfig({
301
300
  tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
302
301
  }),
303
- reserveBeneficiary: reserveBeneficiary,
304
302
  flags: JB721TiersHookFlags({
305
303
  preventOverspending: false,
306
304
  issueTokensForSplits: false,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -246,7 +246,6 @@ contract IssueTokensForSplitsFork is Test {
246
246
  currency: uint32(uint160(NATIVE_TOKEN)),
247
247
  decimals: 18
248
248
  }),
249
- reserveBeneficiary: reserveBeneficiary,
250
249
  flags: JB721TiersHookFlags({
251
250
  preventOverspending: false,
252
251
  issueTokensForSplits: issueTokensForSplits,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/StdInvariant.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {CommonBase} from "forge-std/Base.sol";
5
5
  import {StdCheats} from "forge-std/StdCheats.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -138,7 +138,6 @@ contract Test_ProjectDeployerRulesets is UnitTestSetup {
138
138
  tiersConfig: JB721InitTiersConfig({
139
139
  tiers: tierConfigs, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
140
140
  }),
141
- reserveBeneficiary: reserveBeneficiary,
142
141
  flags: JB721TiersHookFlags({
143
142
  preventOverspending: false,
144
143
  issueTokensForSplits: false,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -29,7 +29,6 @@ contract Test_Getters_Constructor_Unit is UnitTestSetup {
29
29
  tokenUriResolver: IJB721TokenUriResolver(mockTokenUriResolver),
30
30
  contractUri: contractUri,
31
31
  tiersConfig: JB721InitTiersConfig({tiers: tiers, currency: currency, decimals: decimals}),
32
- reserveBeneficiary: address(0),
33
32
  flags: JB721TiersHookFlags({
34
33
  preventOverspending: false,
35
34
  issueTokensForSplits: false,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "../utils/UnitTestSetup.sol";