@bananapus/721-hook-v6 0.0.28 → 0.0.30

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 (46) hide show
  1. package/ADMINISTRATION.md +38 -11
  2. package/ARCHITECTURE.md +53 -99
  3. package/AUDIT_INSTRUCTIONS.md +84 -383
  4. package/CHANGELOG.md +71 -0
  5. package/README.md +79 -225
  6. package/RISKS.md +28 -11
  7. package/SKILLS.md +29 -296
  8. package/STYLE_GUIDE.md +57 -18
  9. package/USER_JOURNEYS.md +57 -501
  10. package/package.json +1 -1
  11. package/references/operations.md +28 -0
  12. package/references/runtime.md +32 -0
  13. package/script/Deploy.s.sol +5 -4
  14. package/src/JB721TiersHook.sol +1 -1
  15. package/src/JB721TiersHookDeployer.sol +1 -1
  16. package/src/JB721TiersHookProjectDeployer.sol +1 -1
  17. package/src/JB721TiersHookStore.sol +23 -17
  18. package/src/libraries/JB721Constants.sol +1 -1
  19. package/src/libraries/JB721TiersRulesetMetadataResolver.sol +1 -1
  20. package/src/libraries/JBBitmap.sol +1 -1
  21. package/src/libraries/JBIpfsDecoder.sol +1 -1
  22. package/src/structs/JB721Tier.sol +5 -11
  23. package/src/structs/JB721TierConfig.sol +5 -20
  24. package/src/structs/JB721TierConfigFlags.sol +26 -0
  25. package/src/structs/JB721TierFlags.sol +17 -0
  26. package/test/721HookAttacks.t.sol +22 -17
  27. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +19 -14
  28. package/test/Fork.t.sol +69 -54
  29. package/test/TestAuditGaps.sol +73 -56
  30. package/test/TestSafeTransferReentrancy.t.sol +4 -4
  31. package/test/TestVotingUnitsLifecycle.t.sol +11 -11
  32. package/test/audit/CodexPayCreditsBypassTierSplits.t.sol +10 -7
  33. package/test/audit/CodexSplitCreditsMismatch.t.sol +10 -7
  34. package/test/fork/ERC20CashOutFork.t.sol +37 -28
  35. package/test/fork/ERC20TierSplitFork.t.sol +28 -21
  36. package/test/fork/IssueTokensForSplitsFork.t.sol +10 -7
  37. package/test/invariants/handlers/TierLifecycleHandler.sol +10 -7
  38. package/test/invariants/handlers/TierStoreHandler.sol +10 -7
  39. package/test/regression/ProjectDeployerRulesets.t.sol +10 -7
  40. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +6 -6
  41. package/test/unit/AuditFixes_Unit.t.sol +37 -28
  42. package/test/unit/adjustTier_Unit.t.sol +268 -202
  43. package/test/unit/getters_constructor_Unit.t.sol +20 -14
  44. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +2 -2
  45. package/test/unit/pay_Unit.t.sol +1 -1
  46. package/CHANGE_LOG.md +0 -359
@@ -0,0 +1,32 @@
1
+ # 721 Hook Runtime
2
+
3
+ ## Contract Roles
4
+
5
+ - [`src/abstract/JB721Hook.sol`](../src/abstract/JB721Hook.sol) is the shared pay and cash-out hook surface. It validates the calling terminal, decodes metadata, and delegates runtime behavior to the concrete hook.
6
+ - [`src/JB721TiersHook.sol`](../src/JB721TiersHook.sol) is the main project-facing contract. It handles tier-aware minting, split forwarding, discount updates, metadata changes, reserve minting, and cash-out weight calculations.
7
+ - [`src/JB721TiersHookStore.sol`](../src/JB721TiersHookStore.sol) is the shared storage and accounting backend. It owns tier definitions, supply counters, burn counts, reserve availability, and voting-unit state.
8
+ - [`src/libraries/JB721TiersHookLib.sol`](../src/libraries/JB721TiersHookLib.sol) holds size-sensitive helper logic such as tier adjustment, split calculation/distribution, pricing normalization, and token-URI resolution.
9
+
10
+ ## Runtime Path
11
+
12
+ 1. Terminal calls [`src/abstract/JB721Hook.sol`](../src/abstract/JB721Hook.sol) through the pay or cash-out hook interface.
13
+ 2. The abstract hook validates the terminal and decodes metadata.
14
+ 3. [`src/JB721TiersHook.sol`](../src/JB721TiersHook.sol) computes pricing, splits, credits, or burn-side cash-out weights.
15
+ 4. [`src/JB721TiersHookStore.sol`](../src/JB721TiersHookStore.sol) records the mint, reserve, burn, and tier-state effects.
16
+ 5. If the flow forwards split funds or resolves token metadata, the hook delegates into [`src/libraries/JB721TiersHookLib.sol`](../src/libraries/JB721TiersHookLib.sol).
17
+
18
+ ## High-Risk Areas
19
+
20
+ - Reserve accounting: edits around `reserveFrequency`, pending reserves, or owner minting must preserve the store's supply protections.
21
+ - Tier splits: split forwarding changes affect both payer economics and project treasury accounting. Check both `beforePayRecordedWith` and the distribution path.
22
+ - Discount behavior: price discounts affect mint eligibility but cash-out weight still tracks the original tier price. Do not conflate the two.
23
+ - Voting units: verify whether a tier uses explicit voting units or falls back to price-based voting power before changing governance-facing math.
24
+ - Tier removal and cleanup: removing tiers is not the same as cleaning the sorted tier list. Storage cleanup behavior matters.
25
+
26
+ ## Tests To Trust First
27
+
28
+ - [`test/invariants/`](../test/invariants/) for broad accounting invariants.
29
+ - [`test/E2E/`](../test/E2E/) for launch and end-to-end payment flows.
30
+ - [`test/regression/`](../test/regression/) for previously broken edge cases.
31
+ - [`test/TestVotingUnitsLifecycle.t.sol`](../test/TestVotingUnitsLifecycle.t.sol) for voting-unit lifecycle behavior.
32
+ - [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol) and [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol) for reentrancy and attack-surface checks.
@@ -1,10 +1,11 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
6
- // forge-lint: disable-next-line(unaliased-plain-import)
7
- import "@bananapus/address-registry-v6/script/helpers/AddressRegistryDeploymentLib.sol";
4
+ import {CoreDeployment, CoreDeploymentLib} from "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
5
+ import {
6
+ AddressRegistryDeployment,
7
+ AddressRegistryDeploymentLib
8
+ } from "@bananapus/address-registry-v6/script/helpers/AddressRegistryDeploymentLib.sol";
8
9
 
9
10
  import {Sphinx} from "@sphinx-labs/contracts/contracts/foundry/SphinxPlugin.sol";
10
11
  import {Script} from "forge-std/Script.sol";
@@ -776,7 +776,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
776
776
  // Transfers must not be paused (when not minting or burning).
777
777
  if (from != address(0)) {
778
778
  // If transfers are pausable, check if they're paused.
779
- if (tier.transfersPausable) {
779
+ if (tier.flags.transfersPausable) {
780
780
  // Get a reference to the project's current ruleset.
781
781
  JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID);
782
782
 
@@ -29,7 +29,7 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
29
29
  IJB721TiersHookStore public immutable STORE;
30
30
 
31
31
  //*********************************************************************//
32
- // ----------------------- internal properties ----------------------- //
32
+ // -------------------- internal stored properties ------------------- //
33
33
  //*********************************************************************//
34
34
 
35
35
  /// @notice This contract's current nonce, used for the Juicebox address registry.
@@ -218,7 +218,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
218
218
  }
219
219
 
220
220
  //*********************************************************************//
221
- // ------------------------ internal functions ----------------------- //
221
+ // ----------------------- internal helpers -------------------------- //
222
222
  //*********************************************************************//
223
223
 
224
224
  /// @notice Launches a project.
@@ -10,6 +10,7 @@ import {JB721Constants} from "./libraries/JB721Constants.sol";
10
10
  import {JBBitmap} from "./libraries/JBBitmap.sol";
11
11
  import {JB721Tier} from "./structs/JB721Tier.sol";
12
12
  import {JB721TierConfig} from "./structs/JB721TierConfig.sol";
13
+ import {JB721TierFlags} from "./structs/JB721TierFlags.sol";
13
14
  import {JB721TiersHookFlags} from "./structs/JB721TiersHookFlags.sol";
14
15
  import {JBBitmapWord} from "./structs/JBBitmapWord.sol";
15
16
  import {JBStored721Tier} from "./structs/JBStored721Tier.sol";
@@ -549,11 +550,13 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
549
550
  encodedIPFSUri: encodedIPFSUriOf[hook][tierId],
550
551
  category: storedTier.category,
551
552
  discountPercent: storedTier.discountPercent,
552
- allowOwnerMint: (packed & 0x1 != 0),
553
- transfersPausable: (packed & 0x2 != 0),
554
- cantBeRemoved: (packed & 0x8 != 0),
555
- cantIncreaseDiscountPercent: (packed & 0x10 != 0),
556
- cantBuyWithCredits: (packed & 0x20 != 0),
553
+ flags: JB721TierFlags({
554
+ allowOwnerMint: (packed & 0x1 != 0),
555
+ transfersPausable: (packed & 0x2 != 0),
556
+ cantBeRemoved: (packed & 0x8 != 0),
557
+ cantIncreaseDiscountPercent: (packed & 0x10 != 0),
558
+ cantBuyWithCredits: (packed & 0x20 != 0)
559
+ }),
557
560
  splitPercent: storedTier.splitPercent,
558
561
  resolvedUri: !includeResolvedUri || tokenUriResolverOf[hook] == IJB721TokenUriResolver(address(0))
559
562
  ? ""
@@ -744,9 +747,11 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
744
747
  uint256 currentSortedTierId = _firstSortedTierIdOf({hook: hook, category: 0});
745
748
 
746
749
  // Keep track of the previous non-removed tier ID.
750
+ // slither-disable-next-line uninitialized-local
747
751
  uint256 previousSortedTierId;
748
752
 
749
753
  // Initialize a `JBBitmapWord` for tracking removed tiers.
754
+ // slither-disable-next-line uninitialized-local
750
755
  JBBitmapWord memory bitmapWord;
751
756
 
752
757
  // Make the sorted array.
@@ -820,6 +825,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
820
825
  uint256 startSortedTierId = currentMaxTierIdOf == 0 ? 0 : _firstSortedTierIdOf({hook: msg.sender, category: 0});
821
826
 
822
827
  // Keep track of the previous tier's ID while iterating.
828
+ // slither-disable-next-line uninitialized-local
823
829
  uint256 previousTierId;
824
830
 
825
831
  // Keep a reference to the 721 contract's flags.
@@ -855,20 +861,20 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
855
861
  // Make sure the new tier doesn't have voting units if the 721 contract's flags don't allow it to.
856
862
  if (
857
863
  flags.noNewTiersWithVotes
858
- && ((tierToAdd.useVotingUnits && tierToAdd.votingUnits != 0)
859
- || (!tierToAdd.useVotingUnits && tierToAdd.price != 0))
864
+ && ((tierToAdd.flags.useVotingUnits && tierToAdd.votingUnits != 0)
865
+ || (!tierToAdd.flags.useVotingUnits && tierToAdd.price != 0))
860
866
  ) {
861
867
  revert JB721TiersHookStore_VotingUnitsNotAllowed(tierId);
862
868
  }
863
869
 
864
870
  // Make sure the new tier doesn't have a reserve frequency if the 721 contract's flags don't allow it to,
865
871
  // OR if manual minting is allowed.
866
- if ((flags.noNewTiersWithReserves || tierToAdd.allowOwnerMint) && tierToAdd.reserveFrequency != 0) {
872
+ if ((flags.noNewTiersWithReserves || tierToAdd.flags.allowOwnerMint) && tierToAdd.reserveFrequency != 0) {
867
873
  revert JB721TiersHookStore_ReserveFrequencyNotAllowed(tierId);
868
874
  }
869
875
 
870
876
  // Make sure the new tier doesn't have owner minting enabled if the 721 contract's flags don't allow it to.
871
- if (flags.noNewTiersWithOwnerMinting && tierToAdd.allowOwnerMint) {
877
+ if (flags.noNewTiersWithOwnerMinting && tierToAdd.flags.allowOwnerMint) {
872
878
  revert JB721TiersHookStore_ManualMintingNotAllowed(tierId);
873
879
  }
874
880
 
@@ -905,17 +911,17 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
905
911
  category: uint24(tierToAdd.category),
906
912
  discountPercent: uint8(tierToAdd.discountPercent),
907
913
  packedBools: _packBools({
908
- allowOwnerMint: tierToAdd.allowOwnerMint,
909
- transfersPausable: tierToAdd.transfersPausable,
910
- useVotingUnits: tierToAdd.useVotingUnits,
911
- cantBeRemoved: tierToAdd.cantBeRemoved,
912
- cantIncreaseDiscountPercent: tierToAdd.cantIncreaseDiscountPercent,
913
- cantBuyWithCredits: tierToAdd.cantBuyWithCredits
914
+ allowOwnerMint: tierToAdd.flags.allowOwnerMint,
915
+ transfersPausable: tierToAdd.flags.transfersPausable,
916
+ useVotingUnits: tierToAdd.flags.useVotingUnits,
917
+ cantBeRemoved: tierToAdd.flags.cantBeRemoved,
918
+ cantIncreaseDiscountPercent: tierToAdd.flags.cantIncreaseDiscountPercent,
919
+ cantBuyWithCredits: tierToAdd.flags.cantBuyWithCredits
914
920
  })
915
921
  });
916
922
 
917
923
  // Store voting units in a separate mapping if custom voting units are used.
918
- if (tierToAdd.useVotingUnits && tierToAdd.votingUnits != 0) {
924
+ if (tierToAdd.flags.useVotingUnits && tierToAdd.votingUnits != 0) {
919
925
  _tierVotingUnitsOf[msg.sender][tierId] = uint32(tierToAdd.votingUnits);
920
926
  }
921
927
 
@@ -928,7 +934,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
928
934
 
929
935
  // Set the reserve beneficiary if needed.
930
936
  if (tierToAdd.reserveBeneficiary != address(0) && tierToAdd.reserveFrequency != 0) {
931
- if (tierToAdd.useReserveBeneficiaryAsDefault) {
937
+ if (tierToAdd.flags.useReserveBeneficiaryAsDefault) {
932
938
  // WARNING: This overwrites the global default for ALL tiers without a tier-specific beneficiary.
933
939
  if (defaultReserveBeneficiaryOf[msg.sender] != tierToAdd.reserveBeneficiary) {
934
940
  defaultReserveBeneficiaryOf[msg.sender] = tierToAdd.reserveBeneficiary;
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.0;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  /// @notice Global constants used across 721 hook contracts.
5
5
  library JB721Constants {
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.17;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JB721TiersRulesetMetadata} from "../structs/JB721TiersRulesetMetadata.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.17;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  import {JBBitmapWord} from "../structs/JBBitmapWord.sol";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.17;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  /// @title JBIpfsDecoder
5
5
  /// @notice Utilities to decode an IPFS hash.
@@ -1,6 +1,8 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.0;
3
3
 
4
+ import {JB721TierFlags} from "./JB721TierFlags.sol";
5
+
4
6
  /// @custom:member id The tier's ID.
5
7
  /// @custom:member price The price to buy an NFT in this tier, in terms of the currency in its `JBInitTiersConfig`.
6
8
  /// @custom:member remainingSupply The remaining number of NFTs which can be minted from this tier.
@@ -13,12 +15,8 @@ pragma solidity ^0.8.0;
13
15
  /// @custom:member encodedIPFSUri The IPFS URI to use for each NFT in this tier.
14
16
  /// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers.
15
17
  /// @custom:member discountPercent The discount that should be applied to the tier.
16
- /// @custom:member allowOwnerMint A boolean indicating whether the contract's owner can mint NFTs from this tier
17
- /// on-demand.
18
- /// @custom:member cantBeRemoved A boolean indicating whether attempts to remove this tier will revert.
19
- /// @custom:member cantIncreaseDiscountPercent If the tier cannot have its discount increased.
20
- /// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits.
21
- /// @custom:member transfersPausable A boolean indicating whether transfers for NFTs in tier can be paused.
18
+ /// @custom:member flags Boolean flags for this tier (allowOwnerMint, transfersPausable, cantBeRemoved,
19
+ /// cantIncreaseDiscountPercent, cantBuyWithCredits).
22
20
  /// @custom:member splitPercent The percentage of the tier's price that gets routed to the project's split group when
23
21
  /// an NFT from this tier is minted. Out of `JBConstants.SPLITS_TOTAL_PERCENT`.
24
22
  /// @custom:member resolvedUri A resolved token URI for NFTs in this tier. Only available if the NFT this tier belongs
@@ -36,11 +34,7 @@ struct JB721Tier {
36
34
  bytes32 encodedIPFSUri;
37
35
  uint24 category;
38
36
  uint8 discountPercent;
39
- bool allowOwnerMint;
40
- bool transfersPausable;
41
- bool cantBeRemoved;
42
- bool cantIncreaseDiscountPercent;
43
- bool cantBuyWithCredits;
37
+ JB721TierFlags flags;
44
38
  uint32 splitPercent;
45
39
  string resolvedUri;
46
40
  }
@@ -3,6 +3,8 @@ pragma solidity ^0.8.0;
3
3
 
4
4
  import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
5
5
 
6
+ import {JB721TierConfigFlags} from "./JB721TierConfigFlags.sol";
7
+
6
8
  /// @notice Config for a single NFT tier within a `JB721TiersHook`.
7
9
  /// @custom:member price The price to buy an NFT in this tier, in terms of the currency in its `JBInitTiersConfig`.
8
10
  /// @custom:member initialSupply The total number of NFTs which can be minted from this tier.
@@ -15,19 +17,8 @@ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
15
17
  /// @custom:member encodedIPFSUri The IPFS URI to use for each NFT in this tier.
16
18
  /// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers.
17
19
  /// @custom:member discountPercent The discount that should be applied to the tier.
18
- /// @custom:member allowOwnerMint A boolean indicating whether the contract's owner can mint NFTs from this tier
19
- /// on-demand.
20
- /// @custom:member useReserveBeneficiaryAsDefault A boolean indicating whether this tier's `reserveBeneficiary` should
21
- /// be stored as the default beneficiary for all tiers. WARNING: Setting this to `true` overwrites the global
22
- /// `defaultReserveBeneficiaryOf` for the hook, which affects ALL existing tiers that do not have a tier-specific
23
- /// reserve beneficiary. Use with caution when calling `adjustTiers` on hooks with existing tiers.
24
- /// @custom:member transfersPausable A boolean indicating whether transfers for NFTs in tier can be paused.
25
- /// @custom:member useVotingUnits A boolean indicating whether the `votingUnits` should be used to calculate voting
26
- /// power. If `useVotingUnits` is false, voting power is based on the tier's price.
27
- /// @custom:member cantBeRemoved If the tier cannot be removed once added.
28
- /// @custom:member cannotIncreaseDiscount If the tier cannot have its discount increased.
29
- /// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits. Only fresh
30
- /// payment value counts toward this tier's price.
20
+ /// @custom:member flags Boolean flags for this tier config (allowOwnerMint, useReserveBeneficiaryAsDefault,
21
+ /// transfersPausable, useVotingUnits, cantBeRemoved, cantIncreaseDiscountPercent, cantBuyWithCredits).
31
22
  /// @custom:member splitPercent The percentage of the tier's price that gets routed to the tier's split group when
32
23
  /// an NFT from this tier is minted. Out of `JBConstants.SPLITS_TOTAL_PERCENT`.
33
24
  /// @custom:member splits The splits to use for this tier's split group. These define where the split portion of the
@@ -43,13 +34,7 @@ struct JB721TierConfig {
43
34
  bytes32 encodedIPFSUri;
44
35
  uint24 category;
45
36
  uint8 discountPercent;
46
- bool allowOwnerMint;
47
- bool useReserveBeneficiaryAsDefault;
48
- bool transfersPausable;
49
- bool useVotingUnits;
50
- bool cantBeRemoved;
51
- bool cantIncreaseDiscountPercent;
52
- bool cantBuyWithCredits;
37
+ JB721TierConfigFlags flags;
53
38
  uint32 splitPercent;
54
39
  JBSplit[] splits;
55
40
  }
@@ -0,0 +1,26 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @custom:member allowOwnerMint A boolean indicating whether the contract's owner can mint NFTs from this tier
5
+ /// on-demand.
6
+ /// @custom:member useReserveBeneficiaryAsDefault A boolean indicating whether this tier's `reserveBeneficiary` should
7
+ /// be stored as the default beneficiary for all tiers. WARNING: Setting this to `true` overwrites the global
8
+ /// `defaultReserveBeneficiaryOf` for the hook, which affects ALL existing tiers that do not have a tier-specific
9
+ /// reserve beneficiary. Use with caution when calling `adjustTiers` on hooks with existing tiers.
10
+ /// @custom:member transfersPausable A boolean indicating whether transfers for NFTs in this tier can be paused.
11
+ /// @custom:member useVotingUnits A boolean indicating whether the `votingUnits` should be used to calculate voting
12
+ /// power. If `useVotingUnits` is false, voting power is based on the tier's price.
13
+ /// @custom:member cantBeRemoved If the tier cannot be removed once added.
14
+ /// @custom:member cantIncreaseDiscountPercent If the tier cannot have its discount increased.
15
+ /// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits. Only fresh
16
+ /// payment value counts toward this tier's price.
17
+ // forge-lint: disable-next-line(pascal-case-struct)
18
+ struct JB721TierConfigFlags {
19
+ bool allowOwnerMint;
20
+ bool useReserveBeneficiaryAsDefault;
21
+ bool transfersPausable;
22
+ bool useVotingUnits;
23
+ bool cantBeRemoved;
24
+ bool cantIncreaseDiscountPercent;
25
+ bool cantBuyWithCredits;
26
+ }
@@ -0,0 +1,17 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ /// @custom:member allowOwnerMint A boolean indicating whether the contract's owner can mint NFTs from this tier
5
+ /// on-demand.
6
+ /// @custom:member transfersPausable A boolean indicating whether transfers for NFTs in this tier can be paused.
7
+ /// @custom:member cantBeRemoved A boolean indicating whether attempts to remove this tier will revert.
8
+ /// @custom:member cantIncreaseDiscountPercent If the tier cannot have its discount increased.
9
+ /// @custom:member cantBuyWithCredits If true, this tier cannot be purchased using accumulated pay credits.
10
+ // forge-lint: disable-next-line(pascal-case-struct)
11
+ struct JB721TierFlags {
12
+ bool allowOwnerMint;
13
+ bool transfersPausable;
14
+ bool cantBeRemoved;
15
+ bool cantIncreaseDiscountPercent;
16
+ bool cantBuyWithCredits;
17
+ }
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./utils/UnitTestSetup.sol";
6
+ import {JB721TierConfigFlags} from "../src/structs/JB721TierConfigFlags.sol";
6
7
 
7
8
  /// @title 721HookAttacks
8
9
  /// @notice Adversarial security tests for JB721TiersHook and JB721TiersHookStore.
@@ -81,13 +82,15 @@ contract NFTHookAttacks is UnitTestSetup {
81
82
  encodedIPFSUri: tokenUris[0],
82
83
  category: 2,
83
84
  discountPercent: 0,
84
- allowOwnerMint: false,
85
- useReserveBeneficiaryAsDefault: false,
86
- transfersPausable: false,
87
- cantBeRemoved: false,
88
- cantIncreaseDiscountPercent: false,
89
- cantBuyWithCredits: false,
90
- useVotingUnits: false,
85
+ flags: JB721TierConfigFlags({
86
+ allowOwnerMint: false,
87
+ useReserveBeneficiaryAsDefault: false,
88
+ transfersPausable: false,
89
+ useVotingUnits: false,
90
+ cantBeRemoved: false,
91
+ cantIncreaseDiscountPercent: false,
92
+ cantBuyWithCredits: false
93
+ }),
91
94
  splitPercent: 0,
92
95
  splits: new JBSplit[](0)
93
96
  });
@@ -116,7 +119,7 @@ contract NFTHookAttacks is UnitTestSetup {
116
119
  /// @notice Set discount to 100%, verify the effective price for the tier.
117
120
  function test_maxDiscountPercent_effectivePrice() public {
118
121
  defaultTierConfig.discountPercent = 0;
119
- defaultTierConfig.cantIncreaseDiscountPercent = false;
122
+ defaultTierConfig.flags.cantIncreaseDiscountPercent = false;
120
123
 
121
124
  JB721TiersHook targetHook = _initHookDefaultTiers(1);
122
125
 
@@ -137,7 +140,7 @@ contract NFTHookAttacks is UnitTestSetup {
137
140
  /// @notice Try to increase discount when the flag forbids it.
138
141
  function test_cantIncreaseDiscountPercent_enforcement() public {
139
142
  defaultTierConfig.discountPercent = 10;
140
- defaultTierConfig.cantIncreaseDiscountPercent = true;
143
+ defaultTierConfig.flags.cantIncreaseDiscountPercent = true;
141
144
 
142
145
  JB721TiersHook targetHook = _initHookDefaultTiers(1);
143
146
 
@@ -213,7 +216,7 @@ contract NFTHookAttacks is UnitTestSetup {
213
216
  function test_cashOutWeight_afterTierRemoval() public {
214
217
  defaultTierConfig.initialSupply = 100;
215
218
  defaultTierConfig.votingUnits = 10;
216
- defaultTierConfig.useVotingUnits = true;
219
+ defaultTierConfig.flags.useVotingUnits = true;
217
220
 
218
221
  ForTest_JB721TiersHook targetHook = _initializeForTestHook(1);
219
222
  IJB721TiersHookStore hookStore = targetHook.STORE();
@@ -369,13 +372,15 @@ contract NFTHookAttacks is UnitTestSetup {
369
372
  encodedIPFSUri: tokenUris[0],
370
373
  category: 1,
371
374
  discountPercent: 0,
372
- allowOwnerMint: true,
373
- useReserveBeneficiaryAsDefault: false,
374
- transfersPausable: false,
375
- cantBeRemoved: false,
376
- cantIncreaseDiscountPercent: false,
377
- cantBuyWithCredits: false,
378
- useVotingUnits: false,
375
+ flags: JB721TierConfigFlags({
376
+ allowOwnerMint: true,
377
+ useReserveBeneficiaryAsDefault: false,
378
+ transfersPausable: false,
379
+ useVotingUnits: false,
380
+ cantBeRemoved: false,
381
+ cantIncreaseDiscountPercent: false,
382
+ cantBuyWithCredits: false
383
+ }),
379
384
  splitPercent: 0,
380
385
  splits: new JBSplit[](0)
381
386
  });
@@ -20,6 +20,7 @@ import "../utils/TestBaseWorkflow.sol";
20
20
  // forge-lint: disable-next-line(unaliased-plain-import)
21
21
  import "../../src/interfaces/IJB721TiersHook.sol";
22
22
  import {MetadataResolverHelper} from "@bananapus/core-v6/test/helpers/MetadataResolverHelper.sol";
23
+ import {JB721TierConfigFlags} from "../../src/structs/JB721TierConfigFlags.sol";
23
24
 
24
25
  contract Test_TiersHook_E2E is TestBaseWorkflow {
25
26
  using JBRulesetMetadataResolver for JBRuleset;
@@ -796,13 +797,15 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
796
797
  encodedIPFSUri: tokenUris[i],
797
798
  category: uint24(100),
798
799
  discountPercent: uint8(0),
799
- allowOwnerMint: false,
800
- useReserveBeneficiaryAsDefault: false,
801
- transfersPausable: false,
802
- useVotingUnits: false,
803
- cantBeRemoved: false,
804
- cantIncreaseDiscountPercent: false,
805
- cantBuyWithCredits: false,
800
+ flags: JB721TierConfigFlags({
801
+ allowOwnerMint: false,
802
+ useReserveBeneficiaryAsDefault: false,
803
+ transfersPausable: false,
804
+ useVotingUnits: false,
805
+ cantBeRemoved: false,
806
+ cantIncreaseDiscountPercent: false,
807
+ cantBuyWithCredits: false
808
+ }),
806
809
  splitPercent: 0,
807
810
  splits: new JBSplit[](0)
808
811
  });
@@ -889,13 +892,15 @@ contract Test_TiersHook_E2E is TestBaseWorkflow {
889
892
  encodedIPFSUri: tokenUris[0],
890
893
  category: uint24(100),
891
894
  discountPercent: _discountPercent,
892
- allowOwnerMint: false,
893
- useReserveBeneficiaryAsDefault: false,
894
- transfersPausable: false,
895
- useVotingUnits: false,
896
- cantBeRemoved: false,
897
- cantIncreaseDiscountPercent: false,
898
- cantBuyWithCredits: false,
895
+ flags: JB721TierConfigFlags({
896
+ allowOwnerMint: false,
897
+ useReserveBeneficiaryAsDefault: false,
898
+ transfersPausable: false,
899
+ useVotingUnits: false,
900
+ cantBeRemoved: false,
901
+ cantIncreaseDiscountPercent: false,
902
+ cantBuyWithCredits: false
903
+ }),
899
904
  splitPercent: 0,
900
905
  splits: new JBSplit[](0)
901
906
  });