@bananapus/721-hook-v6 0.0.14 → 0.0.16

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 (58) hide show
  1. package/ADMINISTRATION.md +4 -3
  2. package/ARCHITECTURE.md +2 -2
  3. package/README.md +2 -2
  4. package/RISKS.md +4 -3
  5. package/SKILLS.md +12 -12
  6. package/STYLE_GUIDE.md +14 -1
  7. package/package.json +5 -5
  8. package/script/Deploy.s.sol +11 -2
  9. package/script/helpers/Hook721DeploymentLib.sol +8 -1
  10. package/src/JB721TiersHook.sol +149 -123
  11. package/src/JB721TiersHookProjectDeployer.sol +4 -3
  12. package/src/JB721TiersHookStore.sol +8 -1
  13. package/src/abstract/ERC721.sol +38 -19
  14. package/src/abstract/JB721Hook.sol +5 -1
  15. package/src/interfaces/IJB721TiersHook.sol +22 -3
  16. package/src/interfaces/IJB721TiersHookStore.sol +3 -0
  17. package/src/libraries/JB721TiersHookLib.sol +156 -34
  18. package/src/libraries/JB721TiersRulesetMetadataResolver.sol +4 -1
  19. package/src/libraries/JBBitmap.sol +1 -0
  20. package/src/libraries/JBIpfsDecoder.sol +4 -1
  21. package/src/structs/JB721InitTiersConfig.sol +1 -5
  22. package/src/structs/JB721Tier.sol +2 -0
  23. package/src/structs/JB721TierConfig.sol +2 -0
  24. package/src/structs/JB721TiersHookFlags.sol +1 -0
  25. package/src/structs/JB721TiersMintReservesConfig.sol +1 -0
  26. package/src/structs/JB721TiersRulesetMetadata.sol +1 -0
  27. package/src/structs/JB721TiersSetDiscountPercentConfig.sol +1 -0
  28. package/src/structs/JBBitmapWord.sol +1 -0
  29. package/src/structs/JBDeploy721TiersHookConfig.sol +1 -0
  30. package/src/structs/JBLaunchProjectConfig.sol +1 -0
  31. package/src/structs/JBLaunchRulesetsConfig.sol +1 -0
  32. package/src/structs/JBPayDataHookRulesetConfig.sol +1 -0
  33. package/src/structs/JBPayDataHookRulesetMetadata.sol +4 -0
  34. package/src/structs/JBQueueRulesetsConfig.sol +1 -0
  35. package/src/structs/JBStored721Tier.sol +1 -0
  36. package/test/721HookAttacks.t.sol +1 -0
  37. package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +30 -12
  38. package/test/Fork.t.sol +60 -11
  39. package/test/fork/ERC20TierSplitFork.t.sol +51 -9
  40. package/test/invariants/TierLifecycleInvariant.t.sol +3 -0
  41. package/test/invariants/TieredHookStoreInvariant.t.sol +5 -0
  42. package/test/invariants/handlers/TierLifecycleHandler.sol +32 -0
  43. package/test/invariants/handlers/TierStoreHandler.sol +3 -0
  44. package/test/regression/CacheTierLookup.t.sol +2 -0
  45. package/test/regression/ReserveBeneficiaryOverwrite.t.sol +1 -0
  46. package/test/regression/SplitNoBeneficiary.t.sol +2 -0
  47. package/test/unit/JB721TiersRulesetMetadataResolver.t.sol +3 -0
  48. package/test/unit/JBBitmap.t.sol +1 -0
  49. package/test/unit/JBIpfsDecoder.t.sol +5 -0
  50. package/test/unit/TierSupplyReserveCheck.t.sol +1 -0
  51. package/test/unit/adjustTier_Unit.t.sol +53 -34
  52. package/test/unit/deployer_Unit.t.sol +11 -0
  53. package/test/unit/getters_constructor_Unit.t.sol +54 -27
  54. package/test/unit/mintFor_mintReservesFor_Unit.t.sol +25 -0
  55. package/test/unit/pay_CrossCurrency_Unit.t.sol +54 -23
  56. package/test/unit/pay_Unit.t.sol +56 -13
  57. package/test/unit/redeem_Unit.t.sol +10 -0
  58. package/test/unit/tierSplitRouting_Unit.t.sol +13 -2
@@ -7,7 +7,6 @@ import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
7
7
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
8
8
  import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
9
9
  import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
10
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
11
10
  import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
12
11
  import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
13
12
  import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
@@ -17,8 +16,6 @@ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
17
16
  import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
18
17
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
19
18
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
20
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
21
- import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
22
19
  import {Context} from "@openzeppelin/contracts/utils/Context.sol";
23
20
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
24
21
  import {JB721Hook} from "./abstract/JB721Hook.sol";
@@ -26,7 +23,6 @@ import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
26
23
  import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
27
24
  import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
28
25
  import {JB721TiersHookLib} from "./libraries/JB721TiersHookLib.sol";
29
- import {mulDiv} from "@prb/math/src/Common.sol";
30
26
  import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
31
27
  import {JB721InitTiersConfig} from "./structs/JB721InitTiersConfig.sol";
32
28
  import {JB721Tier} from "./structs/JB721Tier.sol";
@@ -41,8 +37,6 @@ import {JB721TiersSetDiscountPercentConfig} from "./structs/JB721TiersSetDiscoun
41
37
  /// information specified by the payer. The project's owner can enable NFT cash outs through this hook, allowing
42
38
  /// holders to burn their NFTs to reclaim funds from the project (in proportion to the NFT's price).
43
39
  contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook {
44
- using SafeERC20 for IERC20;
45
-
46
40
  //*********************************************************************//
47
41
  // --------------------------- custom errors ------------------------- //
48
42
  //*********************************************************************//
@@ -59,6 +53,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
59
53
  // --------------- public immutable stored properties ---------------- //
60
54
  //*********************************************************************//
61
55
 
56
+ /// @notice The contract that exposes price feeds for currency conversions.
57
+ IJBPrices public immutable override PRICES;
58
+
62
59
  /// @notice The contract storing and managing project rulesets.
63
60
  IJBRulesets public immutable override RULESETS;
64
61
 
@@ -94,9 +91,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
94
91
 
95
92
  /// @notice Packed context for the pricing of this contract's tiers.
96
93
  /// @dev Packed into a uint256:
97
- /// - currency in bits 0-31 (32 bits),
98
- /// - pricing decimals in bits 32-39 (8 bits), and
99
- /// - prices contract in bits 40-199 (160 bits).
94
+ /// - currency in bits 0-31 (32 bits), and
95
+ /// - pricing decimals in bits 32-39 (8 bits).
100
96
  uint256 internal _packedPricingContext;
101
97
 
102
98
  //*********************************************************************//
@@ -105,6 +101,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
105
101
 
106
102
  /// @param directory A directory of terminals and controllers for projects.
107
103
  /// @param permissions A contract storing permissions.
104
+ /// @param prices A contract that exposes price feeds for currency conversions.
108
105
  /// @param rulesets A contract storing and managing project rulesets.
109
106
  /// @param store The contract which stores the NFT's data.
110
107
  /// @param splits The contract that stores and manages splits.
@@ -112,6 +109,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
112
109
  constructor(
113
110
  IJBDirectory directory,
114
111
  IJBPermissions permissions,
112
+ IJBPrices prices,
115
113
  IJBRulesets rulesets,
116
114
  IJB721TiersHookStore store,
117
115
  IJBSplits splits,
@@ -121,6 +119,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
121
119
  JB721Hook(directory)
122
120
  ERC2771Context(trustedForwarder)
123
121
  {
122
+ PRICES = prices;
124
123
  RULESETS = rulesets;
125
124
  STORE = store;
126
125
  SPLITS = splits;
@@ -146,19 +145,17 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
146
145
  }
147
146
 
148
147
  /// @notice Context for the pricing of this hook's tiers.
149
- /// @dev If the `prices` contract is the zero address, this contract only accepts payments in the `currency` token.
150
148
  /// @return currency The currency used for tier prices.
151
149
  /// @return decimals The amount of decimals being used in tier prices.
152
- /// @return prices The prices contract used to resolve the value of payments in currencies other than `currency`.
153
- function pricingContext() external view override returns (uint256 currency, uint256 decimals, IJBPrices prices) {
150
+ function pricingContext() external view override returns (uint256 currency, uint256 decimals) {
154
151
  // Get a reference to the packed pricing context.
155
152
  uint256 packed = _packedPricingContext;
156
153
  // currency in bits 0-31 (32 bits).
154
+ // forge-lint: disable-next-line(unsafe-typecast)
157
155
  currency = uint256(uint32(packed));
158
156
  // pricing decimals in bits 32-39 (8 bits).
157
+ // forge-lint: disable-next-line(unsafe-typecast)
159
158
  decimals = uint256(uint8(packed >> 32));
160
- // prices contract in bits 40-199 (160 bits).
161
- prices = IJBPrices(address(uint160(packed >> 40)));
162
159
  }
163
160
 
164
161
  //*********************************************************************//
@@ -188,32 +185,30 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
188
185
  hookSpecifications = new JBPayHookSpecification[](1);
189
186
 
190
187
  // Calculate per-tier split amounts via the library.
191
- (uint256 totalSplitAmount, bytes memory splitMetadata) =
192
- JB721TiersHookLib.calculateSplitAmounts(STORE, address(this), METADATA_ID_TARGET, context.metadata);
188
+ (uint256 totalSplitAmount, bytes memory splitMetadata) = JB721TiersHookLib.calculateSplitAmounts({
189
+ store: STORE, hook: address(this), metadataIdTarget: METADATA_ID_TARGET, metadata: context.metadata
190
+ });
193
191
 
194
192
  // Convert split amounts from tier pricing to payment token denomination if currencies differ.
195
193
  if (totalSplitAmount != 0) {
196
- (totalSplitAmount, splitMetadata) = JB721TiersHookLib.convertSplitAmounts(
197
- totalSplitAmount,
198
- splitMetadata,
199
- _packedPricingContext,
200
- context.projectId,
201
- context.amount.currency,
202
- context.amount.decimals
203
- );
194
+ (totalSplitAmount, splitMetadata) = JB721TiersHookLib.convertSplitAmounts({
195
+ totalSplitAmount: totalSplitAmount,
196
+ splitMetadata: splitMetadata,
197
+ packedPricingContext: _packedPricingContext,
198
+ prices: PRICES,
199
+ projectId: context.projectId,
200
+ amountCurrency: context.amount.currency,
201
+ amountDecimals: context.amount.decimals
202
+ });
204
203
  }
205
204
 
206
205
  // Adjust weight so the terminal mints tokens only for the amount that actually enters the project.
207
- if (totalSplitAmount == 0 || STORE.flagsOf(address(this)).issueTokensForSplits) {
208
- // No splits, or hook configured to give full token credit regardless — full weight.
209
- weight = context.weight;
210
- } else if (context.amount.value > totalSplitAmount) {
211
- // Partial splits — scale weight by the fraction that enters the project.
212
- weight = mulDiv(context.weight, context.amount.value - totalSplitAmount, context.amount.value);
213
- } else {
214
- // Splits consume the entire payment — no tokens should be minted.
215
- weight = 0;
216
- }
206
+ weight = JB721TiersHookLib.calculateWeight({
207
+ contextWeight: context.weight,
208
+ amountValue: context.amount.value,
209
+ totalSplitAmount: totalSplitAmount,
210
+ issueTokensForSplits: STORE.flagsOf(address(this)).issueTokensForSplits
211
+ });
217
212
 
218
213
  hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: totalSplitAmount, metadata: splitMetadata});
219
214
  }
@@ -268,8 +263,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
268
263
  packed |= uint256(tiersConfig.currency);
269
264
  // pack the pricing decimals in bits 32-39 (8 bits).
270
265
  packed |= uint256(tiersConfig.decimals) << 32;
271
- // pack the prices contract in bits 40-199 (160 bits).
272
- packed |= uint256(uint160(address(tiersConfig.prices))) << 40;
273
266
  // Store the packed value.
274
267
  // slither-disable-next-line events-maths
275
268
  _packedPricingContext = packed;
@@ -287,9 +280,14 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
287
280
 
288
281
  // Record the tiers in this hook's store and set any tier split groups.
289
282
  if (tiersConfig.tiers.length != 0) {
290
- JB721TiersHookLib.recordAddTiersFor(
291
- STORE, SPLITS, projectId, address(this), _msgSender(), tiersConfig.tiers
292
- );
283
+ JB721TiersHookLib.recordAddTiersFor({
284
+ store: STORE,
285
+ splits: SPLITS,
286
+ projectId: projectId,
287
+ hookAddress: address(this),
288
+ caller: _msgSender(),
289
+ tiersToAdd: tiersConfig.tiers
290
+ });
293
291
  }
294
292
 
295
293
  // Set the flags if needed.
@@ -343,9 +341,15 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
343
341
  });
344
342
 
345
343
  // Delegate to the library (via DELEGATECALL) for tier removal, addition, event emission, and split setting.
346
- JB721TiersHookLib.adjustTiersFor(
347
- STORE, SPLITS, PROJECT_ID, address(this), _msgSender(), tiersToAdd, tierIdsToRemove
348
- );
344
+ JB721TiersHookLib.adjustTiersFor({
345
+ store: STORE,
346
+ splits: SPLITS,
347
+ projectId: PROJECT_ID,
348
+ hookAddress: address(this),
349
+ caller: _msgSender(),
350
+ tiersToAdd: tiersToAdd,
351
+ tierIdsToRemove: tierIdsToRemove
352
+ });
349
353
  }
350
354
 
351
355
  /// @notice Manually mint NFTs from the provided tiers .
@@ -376,7 +380,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
376
380
  uint256 tokenId = tokenIds[i];
377
381
 
378
382
  // Mint the NFT.
379
- _mint(beneficiary, tokenId);
383
+ _mint({to: beneficiary, tokenId: tokenId});
380
384
 
381
385
  emit Mint({
382
386
  tokenId: tokenId, tierId: tierIds[i], beneficiary: beneficiary, totalAmountPaid: 0, caller: _msgSender()
@@ -427,18 +431,24 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
427
431
  }
428
432
  }
429
433
 
430
- /// @notice Update this hook's URI metadata properties.
431
- /// @dev Only this contract's owner can set the metadata.
434
+ /// @notice Update this hook's metadata properties.
435
+ /// @dev Only this contract's owner or an operator with the `SET_721_METADATA` permission can set the metadata.
436
+ /// @param name The new collection name. Send empty to leave unchanged.
437
+ /// @param symbol The new collection symbol. Send empty to leave unchanged.
432
438
  /// @param baseUri The new base URI.
433
439
  /// @param contractUri The new contract URI.
434
440
  /// @param tokenUriResolver The new URI resolver.
435
441
  /// @param encodedIPFSUriTierId The ID of the tier to set the encoded IPFS URI of.
436
442
  /// @param encodedIPFSUri The encoded IPFS URI to set.
437
443
  function setMetadata(
444
+ string calldata name,
445
+ string calldata symbol,
438
446
  string calldata baseUri,
439
447
  string calldata contractUri,
440
448
  IJB721TokenUriResolver tokenUriResolver,
449
+ // forge-lint: disable-next-line(mixed-case-variable)
441
450
  uint256 encodedIPFSUriTierId,
451
+ // forge-lint: disable-next-line(mixed-case-variable)
442
452
  bytes32 encodedIPFSUri
443
453
  )
444
454
  external
@@ -449,6 +459,16 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
449
459
  account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_METADATA
450
460
  });
451
461
 
462
+ if (bytes(name).length != 0) {
463
+ // Store the new collection name.
464
+ _setName(name);
465
+ emit SetName({name: name, caller: _msgSender()});
466
+ }
467
+ if (bytes(symbol).length != 0) {
468
+ // Store the new collection symbol.
469
+ _setSymbol(symbol);
470
+ emit SetSymbol({symbol: symbol, caller: _msgSender()});
471
+ }
452
472
  if (bytes(baseUri).length != 0) {
453
473
  // Store the new base URI.
454
474
  baseURI = baseUri;
@@ -509,7 +529,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
509
529
 
510
530
  // Mint the NFT.
511
531
  // slither-disable-next-line reentrency-events
512
- _mint(reserveBeneficiary, tokenId);
532
+ _mint({to: reserveBeneficiary, tokenId: tokenId});
513
533
  }
514
534
  }
515
535
 
@@ -593,7 +613,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
593
613
 
594
614
  // Mint the NFT.
595
615
  // slither-disable-next-line reentrancy-events
596
- _mint(beneficiary, tokenId);
616
+ _mint({to: beneficiary, tokenId: tokenId});
597
617
  }
598
618
  }
599
619
 
@@ -607,100 +627,106 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
607
627
  uint256 value;
608
628
  {
609
629
  bool valid;
610
- (value, valid) = JB721TiersHookLib.normalizePaymentValue(
611
- _packedPricingContext,
612
- PROJECT_ID,
613
- context.amount.value,
614
- context.amount.currency,
615
- context.amount.decimals
616
- );
630
+ (value, valid) = JB721TiersHookLib.normalizePaymentValue({
631
+ packedPricingContext: _packedPricingContext,
632
+ prices: PRICES,
633
+ projectId: PROJECT_ID,
634
+ amountValue: context.amount.value,
635
+ amountCurrency: context.amount.currency,
636
+ amountDecimals: context.amount.decimals
637
+ });
617
638
  if (!valid) return;
618
639
  }
619
640
 
620
- // Keep a reference to the number of NFT credits the beneficiary already has.
621
- uint256 payCredits = payCreditsOf[context.beneficiary];
641
+ // Scope block to free stack slots before the distributeAll call below.
642
+ {
643
+ // Keep a reference to the number of NFT credits the beneficiary already has.
644
+ uint256 payCredits = payCreditsOf[context.beneficiary];
622
645
 
623
- // Set the leftover amount as the initial value.
624
- uint256 leftoverAmount = value;
646
+ // Set the leftover amount as the initial value.
647
+ uint256 leftoverAmount = value;
625
648
 
626
- // If the payer is the beneficiary, combine their NFT credits with the amount paid.
627
- uint256 unusedPayCredits;
628
- if (context.payer == context.beneficiary) {
629
- leftoverAmount += payCredits;
630
- } else {
631
- // Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
632
- unusedPayCredits = payCredits;
633
- }
649
+ // If the payer is the beneficiary, combine their NFT credits with the amount paid.
650
+ uint256 unusedPayCredits;
651
+ if (context.payer == context.beneficiary) {
652
+ leftoverAmount += payCredits;
653
+ } else {
654
+ // Otherwise, the payer's NFT credits won't be used, and we keep track of the unused credits.
655
+ unusedPayCredits = payCredits;
656
+ }
634
657
 
635
- // Keep a reference to the boolean indicating whether paying more than the price of the NFTs being minted is
636
- // allowed. Defaults to the collection's flag.
637
- bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending;
658
+ // Keep a reference to the boolean indicating whether paying more than the price of the NFTs being minted
659
+ // is allowed. Defaults to the collection's flag.
660
+ bool allowOverspending = !STORE.flagsOf(address(this)).preventOverspending;
638
661
 
639
- // Resolve the metadata.
640
- (bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
641
- id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}), metadata: context.payerMetadata
642
- });
662
+ // Resolve the metadata.
663
+ (bool found, bytes memory metadata) = JBMetadataResolver.getDataFor({
664
+ id: JBMetadataResolver.getId({purpose: "pay", target: METADATA_ID_TARGET}),
665
+ metadata: context.payerMetadata
666
+ });
643
667
 
644
- if (found) {
645
- // Keep a reference to the IDs of the tier be to minted.
646
- uint16[] memory tierIdsToMint;
668
+ if (found) {
669
+ // Keep a reference to the IDs of the tier be to minted.
670
+ uint16[] memory tierIdsToMint;
647
671
 
648
- // Keep a reference to the payer's flag indicating whether overspending is allowed.
649
- bool payerAllowsOverspending;
672
+ // Keep a reference to the payer's flag indicating whether overspending is allowed.
673
+ bool payerAllowsOverspending;
650
674
 
651
- // Decode the metadata.
652
- (payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
675
+ // Decode the metadata.
676
+ (payerAllowsOverspending, tierIdsToMint) = abi.decode(metadata, (bool, uint16[]));
653
677
 
654
- // Make sure overspending is allowed if requested.
655
- if (allowOverspending && !payerAllowsOverspending) {
656
- allowOverspending = false;
657
- }
678
+ // Make sure overspending is allowed if requested.
679
+ if (allowOverspending && !payerAllowsOverspending) {
680
+ allowOverspending = false;
681
+ }
658
682
 
659
- // Mint NFTs from the tiers as specified.
660
- if (tierIdsToMint.length != 0) {
661
- // slither-disable-next-line reentrancy-events,reentrancy-no-eth
662
- leftoverAmount =
663
- _mintAll({amount: leftoverAmount, mintTierIds: tierIdsToMint, beneficiary: context.beneficiary});
683
+ // Mint NFTs from the tiers as specified.
684
+ if (tierIdsToMint.length != 0) {
685
+ // slither-disable-next-line reentrancy-events,reentrancy-no-eth
686
+ leftoverAmount = _mintAll({
687
+ amount: leftoverAmount, mintTierIds: tierIdsToMint, beneficiary: context.beneficiary
688
+ });
689
+ }
664
690
  }
665
- }
666
-
667
- // If overspending isn't allowed, revert.
668
- if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
669
691
 
670
- // Update NFT credits if they changed.
671
- uint256 newPayCredits = leftoverAmount + unusedPayCredits;
672
-
673
- if (newPayCredits != payCredits) {
674
- if (newPayCredits > payCredits) {
675
- emit AddPayCredits({
676
- amount: newPayCredits - payCredits,
677
- newTotalCredits: newPayCredits,
678
- account: context.beneficiary,
679
- caller: _msgSender()
680
- });
681
- } else {
682
- emit UsePayCredits({
683
- amount: payCredits - newPayCredits,
684
- newTotalCredits: newPayCredits,
685
- account: context.beneficiary,
686
- caller: _msgSender()
687
- });
692
+ // If overspending isn't allowed, revert.
693
+ if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
694
+
695
+ // Update NFT credits if they changed.
696
+ uint256 newPayCredits = leftoverAmount + unusedPayCredits;
697
+
698
+ if (newPayCredits != payCredits) {
699
+ if (newPayCredits > payCredits) {
700
+ emit AddPayCredits({
701
+ amount: newPayCredits - payCredits,
702
+ newTotalCredits: newPayCredits,
703
+ account: context.beneficiary,
704
+ caller: _msgSender()
705
+ });
706
+ } else {
707
+ emit UsePayCredits({
708
+ amount: payCredits - newPayCredits,
709
+ newTotalCredits: newPayCredits,
710
+ account: context.beneficiary,
711
+ caller: _msgSender()
712
+ });
713
+ }
714
+
715
+ payCreditsOf[context.beneficiary] = newPayCredits;
688
716
  }
689
-
690
- payCreditsOf[context.beneficiary] = newPayCredits;
691
717
  }
692
718
 
693
719
  // Distribute any forwarded funds to tier split groups.
694
720
  if (context.hookMetadata.length != 0 && context.forwardedAmount.value != 0) {
695
- // For ERC20 tokens, pull from terminal using the allowance it granted via _beforeTransferTo.
696
- if (context.forwardedAmount.token != JBConstants.NATIVE_TOKEN) {
697
- IERC20(context.forwardedAmount.token)
698
- .safeTransferFrom(msg.sender, address(this), context.forwardedAmount.value);
699
- }
700
-
701
- JB721TiersHookLib.distributeAll(
702
- DIRECTORY, SPLITS, PROJECT_ID, address(this), context.forwardedAmount.token, context.hookMetadata
703
- );
721
+ JB721TiersHookLib.distributeAll({
722
+ directory: DIRECTORY,
723
+ splits: SPLITS,
724
+ projectId: PROJECT_ID,
725
+ hookAddress: address(this),
726
+ token: context.forwardedAmount.token,
727
+ amount: context.forwardedAmount.value,
728
+ encodedSplitData: context.hookMetadata
729
+ });
704
730
  }
705
731
  }
706
732
 
@@ -732,7 +758,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
732
758
  JB721Tier memory tier = STORE.tierOfTokenId({hook: address(this), tokenId: tokenId, includeResolvedUri: false});
733
759
 
734
760
  // Record the transfers and keep a reference to where the token is coming from.
735
- from = super._update(to, tokenId, auth);
761
+ from = super._update({to: to, tokenId: tokenId, auth: auth});
736
762
 
737
763
  // Transfers must not be paused (when not minting or burning).
738
764
  if (from != address(0)) {
@@ -124,6 +124,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
124
124
  returns (uint256 rulesetId, IJB721TiersHook hook)
125
125
  {
126
126
  // Get the project's projects contract.
127
+ // forge-lint: disable-next-line(mixed-case-variable)
127
128
  IJBProjects PROJECTS = DIRECTORY.PROJECTS();
128
129
 
129
130
  // Enforce permissions.
@@ -256,7 +257,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
256
257
  pausePay: payDataRulesetConfig.metadata.pausePay,
257
258
  pauseCreditTransfers: payDataRulesetConfig.metadata.pauseCreditTransfers,
258
259
  allowOwnerMinting: payDataRulesetConfig.metadata.allowOwnerMinting,
259
- allowSetCustomToken: false,
260
+ allowSetCustomToken: payDataRulesetConfig.metadata.allowSetCustomToken,
260
261
  allowTerminalMigration: payDataRulesetConfig.metadata.allowTerminalMigration,
261
262
  allowSetTerminals: payDataRulesetConfig.metadata.allowSetTerminals,
262
263
  allowSetController: payDataRulesetConfig.metadata.allowSetController,
@@ -324,7 +325,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
324
325
  pausePay: payDataRulesetConfig.metadata.pausePay,
325
326
  pauseCreditTransfers: payDataRulesetConfig.metadata.pauseCreditTransfers,
326
327
  allowOwnerMinting: payDataRulesetConfig.metadata.allowOwnerMinting,
327
- allowSetCustomToken: false,
328
+ allowSetCustomToken: payDataRulesetConfig.metadata.allowSetCustomToken,
328
329
  allowTerminalMigration: payDataRulesetConfig.metadata.allowTerminalMigration,
329
330
  allowSetTerminals: payDataRulesetConfig.metadata.allowSetTerminals,
330
331
  allowSetController: payDataRulesetConfig.metadata.allowSetController,
@@ -390,7 +391,7 @@ contract JB721TiersHookProjectDeployer is ERC2771Context, JBPermissioned, IJB721
390
391
  pausePay: payDataRulesetConfig.metadata.pausePay,
391
392
  pauseCreditTransfers: payDataRulesetConfig.metadata.pauseCreditTransfers,
392
393
  allowOwnerMinting: payDataRulesetConfig.metadata.allowOwnerMinting,
393
- allowSetCustomToken: false,
394
+ allowSetCustomToken: payDataRulesetConfig.metadata.allowSetCustomToken,
394
395
  allowTerminalMigration: payDataRulesetConfig.metadata.allowTerminalMigration,
395
396
  allowSetTerminals: payDataRulesetConfig.metadata.allowSetTerminals,
396
397
  allowSetController: payDataRulesetConfig.metadata.allowSetController,
@@ -62,6 +62,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
62
62
  /// @custom:param hook The 721 contract that the tier belongs to.
63
63
  /// @custom:param tierId The ID of the tier to get the encoded IPFS URI of.
64
64
  /// @custom:returns The encoded IPFS URI.
65
+ // forge-lint: disable-next-line(mixed-case-variable)
65
66
  mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIPFSUriOf;
66
67
 
67
68
  /// @notice Returns the largest tier ID currently used on the provided 721 contract.
@@ -155,6 +156,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
155
156
  /// @param hook The 721 contract that the encoded IPFS URI belongs to.
156
157
  /// @param tokenId The token ID of the 721 to get the encoded tier IPFS URI of.
157
158
  /// @return The encoded IPFS URI.
159
+ // forge-lint: disable-next-line(mixed-case-function)
158
160
  function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) {
159
161
  return encodedIPFSUriOf[hook][tierIdOfToken(tokenId)];
160
162
  }
@@ -532,6 +534,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
532
534
 
533
535
  // slither-disable-next-line calls-loop
534
536
  return JB721Tier({
537
+ // forge-lint: disable-next-line(unsafe-typecast)
535
538
  id: uint32(tierId),
536
539
  price: storedTier.price,
537
540
  remainingSupply: storedTier.remainingSupply,
@@ -1067,7 +1070,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1067
1070
 
1068
1071
  // Apply a discount if needed.
1069
1072
  if (storedTier.discountPercent > 0) {
1070
- price -= mulDiv(price, storedTier.discountPercent, JB721Constants.DISCOUNT_DENOMINATOR);
1073
+ price -= mulDiv({
1074
+ x: price, y: storedTier.discountPercent, denominator: JB721Constants.DISCOUNT_DENOMINATOR
1075
+ });
1071
1076
  }
1072
1077
 
1073
1078
  // Make sure the `amount` is greater than or equal to the tier's price.
@@ -1178,12 +1183,14 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1178
1183
  }
1179
1184
 
1180
1185
  // Set the discount.
1186
+ // forge-lint: disable-next-line(unsafe-typecast)
1181
1187
  storedTier.discountPercent = uint8(discountPercent);
1182
1188
  }
1183
1189
 
1184
1190
  /// @notice Record a new encoded IPFS URI for a tier.
1185
1191
  /// @param tierId The ID of the tier to set the encoded IPFS URI of.
1186
1192
  /// @param encodedIPFSUri The encoded IPFS URI to set for the tier.
1193
+ // forge-lint: disable-next-line(mixed-case-function, mixed-case-variable)
1187
1194
  function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external override {
1188
1195
  encodedIPFSUriOf[msg.sender][tierId] = encodedIPFSUri;
1189
1196
  }