@bananapus/721-hook-v6 0.0.50 → 0.0.52

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.
@@ -71,7 +71,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
71
71
  /// @custom:param hook The 721 contract that the tier belongs to.
72
72
  /// @custom:param tierId The ID of the tier to get the encoded IPFS URI of.
73
73
  /// @custom:returns The encoded IPFS URI.
74
- mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIPFSUriOf;
74
+ mapping(address hook => mapping(uint256 tierId => bytes32)) public override encodedIpfsUriOf;
75
75
 
76
76
  /// @notice Returns the largest tier ID currently used on the provided 721 contract.
77
77
  /// @dev This may not include the last tier ID if it has been removed.
@@ -166,7 +166,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
166
166
  /// @return The encoded IPFS URI for the NFT's tier.
167
167
  // forge-lint: disable-next-line(mixed-case-function)
168
168
  function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) {
169
- return encodedIPFSUriOf[hook][tierIdOfToken(tokenId)];
169
+ return encodedIpfsUriOf[hook][tierIdOfToken(tokenId)];
170
170
  }
171
171
 
172
172
  /// @notice Get the behavioral flags for a hook — such as whether transfers are pausable, whether NFT holders can
@@ -628,7 +628,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
628
628
  // No reserve frequency if there is no reserve beneficiary.
629
629
  reserveFrequency: reserveBeneficiary == address(0) ? 0 : storedTier.reserveFrequency,
630
630
  reserveBeneficiary: reserveBeneficiary,
631
- encodedIPFSUri: encodedIPFSUriOf[hook][tierId],
631
+ encodedIpfsUri: encodedIpfsUriOf[hook][tierId],
632
632
  category: storedTier.category,
633
633
  discountPercent: storedTier.discountPercent,
634
634
  flags: JB721TierFlags({
@@ -1051,9 +1051,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1051
1051
  }
1052
1052
  }
1053
1053
 
1054
- // Set the `encodedIPFSUri` if needed.
1055
- if (tierToAdd.encodedIPFSUri != bytes32(0)) {
1056
- encodedIPFSUriOf[msg.sender][tierId] = tierToAdd.encodedIPFSUri;
1054
+ // Set the `encodedIpfsUri` if needed.
1055
+ if (tierToAdd.encodedIpfsUri != bytes32(0)) {
1056
+ encodedIpfsUriOf[msg.sender][tierId] = tierToAdd.encodedIpfsUri;
1057
1057
  }
1058
1058
 
1059
1059
  if (startSortedTierId != 0) {
@@ -1392,10 +1392,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1392
1392
 
1393
1393
  /// @notice Record a new encoded IPFS URI for a tier.
1394
1394
  /// @param tierId The ID of the tier to set the encoded IPFS URI of.
1395
- /// @param encodedIPFSUri The encoded IPFS URI to set for the tier.
1395
+ /// @param encodedIpfsUri The encoded IPFS URI to set for the tier.
1396
1396
  // forge-lint: disable-next-line(mixed-case-function)
1397
- function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external override {
1398
- encodedIPFSUriOf[msg.sender][tierId] = encodedIPFSUri;
1397
+ function recordSetEncodedIpfsUriOf(uint256 tierId, bytes32 encodedIpfsUri) external override {
1398
+ encodedIpfsUriOf[msg.sender][tierId] = encodedIpfsUri;
1399
1399
  }
1400
1400
 
1401
1401
  /// @notice Record a newly set token URI resolver.
@@ -40,9 +40,9 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
40
40
  /**
41
41
  * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
42
42
  */
43
- function _initialize(string memory name_, string memory symbol_) internal {
44
- _name = name_;
45
- _symbol = symbol_;
43
+ function _initialize(string memory collectionName, string memory collectionSymbol) internal {
44
+ _name = collectionName;
45
+ _symbol = collectionSymbol;
46
46
  }
47
47
 
48
48
  /**
@@ -50,7 +50,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
50
50
  //*********************************************************************//
51
51
 
52
52
  /// @notice The ID of the project that this contract is associated with.
53
- uint256 public override PROJECT_ID;
53
+ uint256 public override projectId;
54
54
 
55
55
  //*********************************************************************//
56
56
  // -------------------------- constructor ---------------------------- //
@@ -194,16 +194,16 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
194
194
  override
195
195
  {
196
196
  // Keep a reference to the project ID.
197
- uint256 projectId = PROJECT_ID;
197
+ uint256 localProjectId = projectId;
198
198
 
199
199
  // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
200
200
  // interaction with the correct project.
201
201
  if (
202
- msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
203
- || context.projectId != projectId
202
+ msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: localProjectId, terminal: IJBTerminal(msg.sender)})
203
+ || context.projectId != localProjectId
204
204
  ) {
205
205
  revert JB721Hook_InvalidCashOut({
206
- caller: msg.sender, contextProjectId: context.projectId, projectId: projectId, msgValue: msg.value
206
+ caller: msg.sender, contextProjectId: context.projectId, projectId: localProjectId, msgValue: msg.value
207
207
  });
208
208
  }
209
209
 
@@ -243,15 +243,17 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
243
243
  /// @dev Reverts if the calling contract is not one of the project's terminals.
244
244
  /// @param context The payment context passed in by the terminal.
245
245
  function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
246
- uint256 projectId = PROJECT_ID;
246
+ uint256 localProjectId = projectId;
247
247
 
248
248
  // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
249
249
  // interaction with the correct project.
250
250
  if (
251
- !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
252
- || context.projectId != projectId
251
+ !DIRECTORY.isTerminalOf({projectId: localProjectId, terminal: IJBTerminal(msg.sender)})
252
+ || context.projectId != localProjectId
253
253
  ) {
254
- revert JB721Hook_InvalidPay({caller: msg.sender, contextProjectId: context.projectId, projectId: projectId});
254
+ revert JB721Hook_InvalidPay({
255
+ caller: msg.sender, contextProjectId: context.projectId, projectId: localProjectId
256
+ });
255
257
  }
256
258
 
257
259
  // Process the payment.
@@ -267,12 +269,12 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
267
269
  function _didBurn(uint256[] memory tokenIds) internal virtual;
268
270
 
269
271
  /// @notice Initializes the contract by associating it with a project and adding ERC721 details.
270
- /// @param projectId The ID of the project that this contract is associated with.
272
+ /// @param initialProjectId The ID of the project that this contract is associated with.
271
273
  /// @param name The name of the NFT collection.
272
274
  /// @param symbol The symbol representing the NFT collection.
273
- function _initialize(uint256 projectId, string memory name, string memory symbol) internal {
274
- ERC721._initialize({name_: name, symbol_: symbol});
275
- PROJECT_ID = projectId;
275
+ function _initialize(uint256 initialProjectId, string memory name, string memory symbol) internal {
276
+ ERC721._initialize({collectionName: name, collectionSymbol: symbol});
277
+ projectId = initialProjectId;
276
278
  }
277
279
 
278
280
  /// @notice Process a received payment by minting NFTs and/or updating credits. Subclasses implement the
@@ -11,7 +11,7 @@ interface IJB721Checkpoints is IERC5805 {
11
11
  /// @notice The hook that this module tracks voting power for.
12
12
  /// @return The hook address.
13
13
  // forge-lint: disable-next-line(mixed-case-function)
14
- function HOOK() external view returns (address);
14
+ function hook() external view returns (address);
15
15
 
16
16
  /// @notice The owner of an NFT at a past block.
17
17
  /// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
@@ -36,8 +36,8 @@ interface IJB721Checkpoints is IERC5805 {
36
36
 
37
37
  /// @notice Initializes a cloned module with its hook reference.
38
38
  /// @dev Can only be called once. Called by the deployer after cloning.
39
- /// @param hook The hook this module serves.
40
- function initialize(address hook) external;
39
+ /// @param hookAddress The hook this module serves.
40
+ function initialize(address hookAddress) external;
41
41
 
42
42
  /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
43
43
  /// @dev Looks up the token's tier voting units from the store internally.
@@ -18,5 +18,5 @@ interface IJB721Hook is IJBRulesetDataHook, IJBPayHook, IJBCashOutHook {
18
18
 
19
19
  /// @notice The ID of the project that this contract is associated with.
20
20
  /// @return The project ID.
21
- function PROJECT_ID() external view returns (uint256);
21
+ function projectId() external view returns (uint256);
22
22
  }
@@ -89,7 +89,7 @@ interface IJB721TiersHook is IJB721Hook {
89
89
  /// @param tierId The ID of the tier whose encoded IPFS URI was set.
90
90
  /// @param encodedUri The new encoded IPFS URI.
91
91
  /// @param caller The address that called the function.
92
- event SetEncodedIPFSUri(uint256 indexed tierId, bytes32 encodedUri, address caller);
92
+ event SetEncodedIpfsUri(uint256 indexed tierId, bytes32 encodedUri, address caller);
93
93
 
94
94
  /// @notice Emitted when the token URI resolver is set.
95
95
  /// @param resolver The new token URI resolver.
@@ -140,8 +140,7 @@ interface IJB721TiersHook is IJB721Hook {
140
140
  /// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
141
141
  /// @dev Deployed lazily on first mint. Pass this to JBTokenDistributor as the IVotes token.
142
142
  /// @return The checkpoint module.
143
- // forge-lint: disable-next-line(mixed-case-function)
144
- function CHECKPOINTS() external view returns (IJB721Checkpoints);
143
+ function checkpoints() external view returns (IJB721Checkpoints);
145
144
 
146
145
  /// @notice The contract that exposes price feeds for currency conversions.
147
146
  /// @return The prices contract.
@@ -165,7 +164,7 @@ interface IJB721TiersHook is IJB721Hook {
165
164
  function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external;
166
165
 
167
166
  /// @notice Initializes a cloned copy of the original `JB721TiersHook` contract.
168
- /// @param projectId The ID of the project this hook is associated with.
167
+ /// @param initialProjectId The ID of the project this hook is associated with.
169
168
  /// @param name The name of the NFT collection.
170
169
  /// @param symbol The symbol representing the NFT collection.
171
170
  /// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
@@ -174,7 +173,7 @@ interface IJB721TiersHook is IJB721Hook {
174
173
  /// @param tiersConfig The NFT tiers and pricing context to initialize the hook with.
175
174
  /// @param flags A set of additional options which dictate how the hook behaves.
176
175
  function initialize(
177
- uint256 projectId,
176
+ uint256 initialProjectId,
178
177
  string memory name,
179
178
  string memory symbol,
180
179
  string memory baseUri,
@@ -217,16 +216,16 @@ interface IJB721TiersHook is IJB721Hook {
217
216
  /// @param tokenUriResolver The new URI resolver. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value
218
217
  /// to leave unchanged. `address(this)` is used instead of `address(0)` because `address(0)` is a valid value that
219
218
  /// clears the resolver.
220
- /// @param encodedIPFSUriTierId The ID of the tier to set the encoded IPFS URI of.
221
- /// @param encodedIPFSUri The encoded IPFS URI to set.
219
+ /// @param encodedIpfsUriTierId The ID of the tier to set the encoded IPFS URI of.
220
+ /// @param encodedIpfsUri The encoded IPFS URI to set.
222
221
  function setMetadata(
223
222
  string calldata name,
224
223
  string calldata symbol,
225
224
  string calldata baseUri,
226
225
  string calldata contractUri,
227
226
  IJB721TokenUriResolver tokenUriResolver,
228
- uint256 encodedIPFSUriTierId,
229
- bytes32 encodedIPFSUri
227
+ uint256 encodedIpfsUriTierId,
228
+ bytes32 encodedIpfsUri
230
229
  )
231
230
  external;
232
231
  }
@@ -42,7 +42,7 @@ interface IJB721TiersHookStore {
42
42
  /// @param tierId The ID of the tier to get the encoded IPFS URI of.
43
43
  /// @return The encoded IPFS URI.
44
44
  // forge-lint: disable-next-line(mixed-case-function)
45
- function encodedIPFSUriOf(address hook, uint256 tierId) external view returns (bytes32);
45
+ function encodedIpfsUriOf(address hook, uint256 tierId) external view returns (bytes32);
46
46
 
47
47
  /// @notice The encoded IPFS URI for the tier of the 721 with the provided token ID.
48
48
  /// @param hook The 721 contract that the encoded IPFS URI belongs to.
@@ -246,9 +246,9 @@ interface IJB721TiersHookStore {
246
246
 
247
247
  /// @notice Record a new encoded IPFS URI for a tier.
248
248
  /// @param tierId The ID of the tier to set the encoded IPFS URI of.
249
- /// @param encodedIPFSUri The encoded IPFS URI to set for the tier.
249
+ /// @param encodedIpfsUri The encoded IPFS URI to set for the tier.
250
250
  // forge-lint: disable-next-line(mixed-case-function)
251
- function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external;
251
+ function recordSetEncodedIpfsUriOf(uint256 tierId, bytes32 encodedIpfsUri) external;
252
252
 
253
253
  /// @notice Record a newly set token URI resolver.
254
254
  /// @param resolver The resolver to set.
@@ -360,7 +360,7 @@ library JB721TiersHookLib {
360
360
  // attributable to tier splits. Downstream compositors (e.g. JBOmnichainDeployer) use this
361
361
  // to preserve split credit when an extra hook (buyback) returns weight=0.
362
362
  if (totalSplitAmount != 0 && context.amount.value != 0 && store.flagsOf(address(this)).issueTokensForSplits) {
363
- splitCreditWeight = mulDiv(context.weight, totalSplitAmount, context.amount.value);
363
+ splitCreditWeight = mulDiv({x: context.weight, y: totalSplitAmount, denominator: context.amount.value});
364
364
  }
365
365
 
366
366
  // Resolve the effective beneficiary from payment metadata.
@@ -690,6 +690,149 @@ library JB721TiersHookLib {
690
690
  // ----------------------- private helpers --------------------------- //
691
691
  //*********************************************************************//
692
692
 
693
+ /// @notice Approves `terminal` to spend `amount` of `token`, calls `addToBalanceOf`, then revokes any
694
+ /// unconsumed allowance and reports the actual amount consumed via the allowance delta.
695
+ /// @dev Allowance-delta consumption protects against terminals that return successfully without pulling
696
+ /// the full granted amount, and the unconditional final `forceApprove(..., 0)` ensures no stale allowance
697
+ /// is left behind on either the success or failure path.
698
+ /// @param terminal The terminal to call.
699
+ /// @param token The ERC-20 token. Native must not be passed here.
700
+ /// @param projectId The destination project ID.
701
+ /// @param amount The amount to grant as allowance and request the terminal to pull.
702
+ /// @return consumed The amount actually consumed by the terminal (0 on revert; otherwise
703
+ /// `amount - remainingAllowance` after the call).
704
+ /// @return failureReason The revert reason from the terminal call, or empty bytes if the call succeeded.
705
+ function _approveAndAddToBalance(
706
+ IJBTerminal terminal,
707
+ address token,
708
+ uint256 projectId,
709
+ uint256 amount
710
+ )
711
+ private
712
+ returns (uint256 consumed, bytes memory failureReason)
713
+ {
714
+ // Grant the terminal allowance to pull up to `amount` of `token` from this contract. `forceApprove`
715
+ // sets the allowance to exactly `amount` regardless of any prior allowance, so a leftover non-zero
716
+ // allowance from a previous failed call cannot inflate this one.
717
+ SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
718
+
719
+ // Track whether the external call returned successfully. Initialized to `false` so a revert path
720
+ // leaves it unset and the consumption math below is skipped.
721
+ bool succeeded;
722
+
723
+ // Call the terminal's `addToBalanceOf` inside try/catch so a reverting terminal can't brick the
724
+ // surrounding payment. The terminal is expected to `transferFrom` against the allowance set above.
725
+ try terminal.addToBalanceOf({
726
+ projectId: projectId,
727
+ token: token,
728
+ amount: amount,
729
+ // Don't release any held fees on this call — fee returns are handled separately.
730
+ shouldReturnHeldFees: false,
731
+ // No memo or metadata is forwarded; the leftover routing is opaque to off-chain consumers.
732
+ memo: "",
733
+ metadata: bytes("")
734
+ }) {
735
+ // The terminal returned without reverting. The actual amount it consumed is measured below
736
+ // via the allowance delta — `succeeded` only records that there was no revert to report.
737
+ succeeded = true;
738
+ } catch (bytes memory reason) {
739
+ // The terminal reverted. Capture the reason so the caller can surface it via the
740
+ // `SplitPayoutReverted` event (or, for the leftover fallback, the
741
+ // `JB721TiersHookLib_SplitFallbackFailed` error) without re-throwing here.
742
+ failureReason = reason;
743
+ }
744
+
745
+ // On success, compute how much the terminal actually pulled. The post-call allowance equals
746
+ // `amount - actuallyConsumed` (since `forceApprove` set the pre-call allowance to `amount`), so
747
+ // `consumed = amount - postAllowance`. On failure, `succeeded` is `false` and `consumed` stays at
748
+ // its default `0`, which lets the caller route the full `amount` to the project's leftover balance.
749
+ if (succeeded) {
750
+ // Safe to use `unchecked`: `postAllowance` cannot exceed `amount` because the terminal can
751
+ // only decrement (via `transferFrom`) the allowance we just set to `amount`.
752
+ unchecked {
753
+ consumed = amount - IERC20(token).allowance({owner: address(this), spender: address(terminal)});
754
+ }
755
+ }
756
+
757
+ // Unconditionally revoke any remaining allowance. This is the load-bearing safety step — without
758
+ // it, a terminal that returns successfully without pulling the full `amount` would leave a stale
759
+ // non-zero allowance that the terminal could drain later. Running on both the success and failure
760
+ // paths means no allowance ever leaks out of this function.
761
+ SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
762
+ }
763
+
764
+ /// @notice Approves `terminal` to spend `amount` of `token`, calls `pay`, then revokes any unconsumed
765
+ /// allowance and reports the actual amount consumed via the allowance delta.
766
+ /// @dev Mirrors `_approveAndAddToBalance` for the `pay` entry point.
767
+ /// @param terminal The terminal to call.
768
+ /// @param token The ERC-20 token. Native must not be passed here.
769
+ /// @param projectId The destination project ID.
770
+ /// @param amount The amount to grant as allowance and request the terminal to pull.
771
+ /// @param beneficiary The beneficiary for the `pay` call.
772
+ /// @return consumed The amount actually consumed by the terminal.
773
+ /// @return failureReason The revert reason from the terminal call, or empty bytes on success.
774
+ function _approveAndPay(
775
+ IJBTerminal terminal,
776
+ address token,
777
+ uint256 projectId,
778
+ uint256 amount,
779
+ address payable beneficiary
780
+ )
781
+ private
782
+ returns (uint256 consumed, bytes memory failureReason)
783
+ {
784
+ // Grant the terminal allowance to pull up to `amount` of `token` from this contract. `forceApprove`
785
+ // sets the allowance to exactly `amount` regardless of any prior allowance, so a leftover non-zero
786
+ // allowance from a previous failed call cannot inflate this one.
787
+ SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
788
+
789
+ // Track whether the external call returned successfully. Initialized to `false` so a revert path
790
+ // leaves it unset and the consumption math below is skipped.
791
+ bool succeeded;
792
+
793
+ // Call the terminal's `pay` inside try/catch so a reverting terminal can't brick the surrounding
794
+ // payment. The terminal is expected to `transferFrom` against the allowance set above and mint
795
+ // project tokens to `beneficiary` in return.
796
+ try terminal.pay({
797
+ projectId: projectId,
798
+ token: token,
799
+ amount: amount,
800
+ beneficiary: beneficiary,
801
+ // No minimum is enforced on the terminal-side mint — the split-level minimum (if any) is
802
+ // enforced by the caller before reaching this helper.
803
+ minReturnedTokens: 0,
804
+ // No memo or metadata is forwarded; the leftover routing is opaque to off-chain consumers.
805
+ memo: "",
806
+ metadata: bytes("")
807
+ }) {
808
+ // The terminal returned without reverting. The actual amount it consumed is measured below
809
+ // via the allowance delta — `succeeded` only records that there was no revert to report.
810
+ succeeded = true;
811
+ } catch (bytes memory reason) {
812
+ // The terminal reverted. Capture the reason so the caller can surface it via the
813
+ // `SplitPayoutReverted` event without re-throwing here.
814
+ failureReason = reason;
815
+ }
816
+
817
+ // On success, compute how much the terminal actually pulled. The post-call allowance equals
818
+ // `amount - actuallyConsumed` (since `forceApprove` set the pre-call allowance to `amount`), so
819
+ // `consumed = amount - postAllowance`. On failure, `succeeded` is `false` and `consumed` stays at
820
+ // its default `0`, which lets the caller route the full `amount` to the project's leftover balance.
821
+ if (succeeded) {
822
+ // Safe to use `unchecked`: `postAllowance` cannot exceed `amount` because the terminal can
823
+ // only decrement (via `transferFrom`) the allowance we just set to `amount`.
824
+ unchecked {
825
+ consumed = amount - IERC20(token).allowance({owner: address(this), spender: address(terminal)});
826
+ }
827
+ }
828
+
829
+ // Unconditionally revoke any remaining allowance. This is the load-bearing safety step — without
830
+ // it, a terminal that returns successfully without pulling the full `amount` would leave a stale
831
+ // non-zero allowance that the terminal could drain later. Running on both the success and failure
832
+ // paths means no allowance ever leaks out of this function.
833
+ SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
834
+ }
835
+
693
836
  /// @notice Distributes funds for a single tier's split group.
694
837
  /// @dev `_sendPayoutToSplit` returns the actual amount that left this contract — `payoutAmount` on full
695
838
  /// success, `0` when the split fully fails (revert or missing recipient), or a partial value when an ERC-20
@@ -779,20 +922,16 @@ library JB721TiersHookLib {
779
922
  });
780
923
  }
781
924
  } else {
782
- SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: leftoverAmount});
783
- try terminal.addToBalanceOf({
784
- projectId: projectId,
785
- token: token,
786
- amount: leftoverAmount,
787
- shouldReturnHeldFees: false,
788
- memo: "",
789
- metadata: bytes("")
790
- }) {}
791
- catch (bytes memory reason) {
792
- // Reset approval on failure.
793
- SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
925
+ // Allowance-delta + unconditional cleanup: a terminal that returns successfully without pulling
926
+ // the full allowance would otherwise leave funds stranded in this library/hook and leak a
927
+ // non-zero allowance to the terminal. Fail closed: revert if either the call reverts OR the
928
+ // terminal under-consumed the granted allowance.
929
+ (uint256 consumed, bytes memory failureReason) = _approveAndAddToBalance({
930
+ terminal: terminal, token: token, projectId: projectId, amount: leftoverAmount
931
+ });
932
+ if (failureReason.length != 0 || consumed != leftoverAmount) {
794
933
  revert JB721TiersHookLib_SplitFallbackFailed({
795
- projectId: projectId, token: token, amount: leftoverAmount, reason: reason
934
+ projectId: projectId, token: token, amount: leftoverAmount, reason: failureReason
796
935
  });
797
936
  }
798
937
  }
@@ -895,24 +1034,24 @@ library JB721TiersHookLib {
895
1034
  return 0;
896
1035
  }
897
1036
  } else {
898
- SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
899
- try terminal.addToBalanceOf({
900
- projectId: split.projectId,
901
- token: token,
902
- amount: amount,
903
- shouldReturnHeldFees: false,
904
- memo: "",
905
- metadata: bytes("")
906
- }) {
907
- return amount;
908
- } catch (bytes memory reason) {
909
- // Reset approval on failure so tokens aren't left approved to the terminal.
910
- SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
1037
+ // Allowance-delta + unconditional cleanup. `sent` reflects the amount the terminal actually
1038
+ // pulled; the unconsumed remainder (if any) stays in this contract and is routed back to the
1039
+ // project's leftover balance by the caller. On revert, `sent` stays 0 and the SplitPayoutReverted
1040
+ // event records the failure reason.
1041
+ bytes memory failureReason;
1042
+ (sent, failureReason) = _approveAndAddToBalance({
1043
+ terminal: terminal, token: token, projectId: split.projectId, amount: amount
1044
+ });
1045
+ if (failureReason.length != 0) {
911
1046
  emit SplitPayoutReverted({
912
- projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
1047
+ projectId: projectId,
1048
+ split: split,
1049
+ amount: amount,
1050
+ reason: failureReason,
1051
+ caller: msg.sender
913
1052
  });
914
- return 0;
915
1053
  }
1054
+ return sent;
916
1055
  }
917
1056
  } else {
918
1057
  if (isNativeToken) {
@@ -933,25 +1072,25 @@ library JB721TiersHookLib {
933
1072
  return 0;
934
1073
  }
935
1074
  } else {
936
- SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
937
- try terminal.pay({
938
- projectId: split.projectId,
1075
+ // Same allowance-delta pattern as the `preferAddToBalance` branch, for the `pay` entry point.
1076
+ bytes memory failureReason;
1077
+ (sent, failureReason) = _approveAndPay({
1078
+ terminal: terminal,
939
1079
  token: token,
1080
+ projectId: split.projectId,
940
1081
  amount: amount,
941
- beneficiary: split.beneficiary,
942
- minReturnedTokens: 0,
943
- memo: "",
944
- metadata: bytes("")
945
- }) {
946
- return amount;
947
- } catch (bytes memory reason) {
948
- // Reset approval on failure so tokens aren't left approved to the terminal.
949
- SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
1082
+ beneficiary: split.beneficiary
1083
+ });
1084
+ if (failureReason.length != 0) {
950
1085
  emit SplitPayoutReverted({
951
- projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
1086
+ projectId: projectId,
1087
+ split: split,
1088
+ amount: amount,
1089
+ reason: failureReason,
1090
+ caller: msg.sender
952
1091
  });
953
- return 0;
954
1092
  }
1093
+ return sent;
955
1094
  }
956
1095
  }
957
1096
  } else if (split.beneficiary != address(0)) {
@@ -12,7 +12,7 @@ import {JB721TierFlags} from "./JB721TierFlags.sol";
12
12
  /// tier. With a `reserveFrequency` of 5, an extra NFT will be minted for the `reserveBeneficiary` for every 5 NFTs
13
13
  /// purchased.
14
14
  /// @custom:member reserveBeneficiary The address which receives any reserve NFTs from this tier.
15
- /// @custom:member encodedIPFSUri The IPFS URI to use for each NFT in this tier.
15
+ /// @custom:member encodedIpfsUri The IPFS URI to use for each NFT in this tier.
16
16
  /// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers.
17
17
  /// @custom:member discountPercent The discount that should be applied to the tier.
18
18
  /// @custom:member flags Boolean flags for this tier (allowOwnerMint, transfersPausable, cantBeRemoved,
@@ -29,7 +29,7 @@ struct JB721Tier {
29
29
  uint104 votingUnits;
30
30
  uint16 reserveFrequency;
31
31
  address reserveBeneficiary;
32
- bytes32 encodedIPFSUri;
32
+ bytes32 encodedIpfsUri;
33
33
  uint24 category;
34
34
  uint8 discountPercent;
35
35
  JB721TierFlags flags;
@@ -14,7 +14,7 @@ import {JB721TierConfigFlags} from "./JB721TierConfigFlags.sol";
14
14
  /// purchased.
15
15
  /// @custom:member reserveBeneficiary The address which receives any reserve NFTs from this tier. Overrides the default
16
16
  /// reserve beneficiary if one is set.
17
- /// @custom:member encodedIPFSUri The IPFS URI to use for each NFT in this tier.
17
+ /// @custom:member encodedIpfsUri The IPFS URI to use for each NFT in this tier.
18
18
  /// @custom:member category The category that NFTs in this tier belongs to. Used to group NFT tiers.
19
19
  /// @custom:member discountPercent The discount that should be applied to the tier.
20
20
  /// @custom:member flags Boolean flags for this tier config (allowOwnerMint, useReserveBeneficiaryAsDefault,
@@ -29,7 +29,7 @@ struct JB721TierConfig {
29
29
  uint32 votingUnits;
30
30
  uint16 reserveFrequency;
31
31
  address reserveBeneficiary;
32
- bytes32 encodedIPFSUri;
32
+ bytes32 encodedIpfsUri;
33
33
  uint24 category;
34
34
  uint8 discountPercent;
35
35
  JB721TierConfigFlags flags;
@@ -24,8 +24,6 @@ pragma solidity ^0.8.0;
24
24
  /// @custom:member holdFees A flag indicating if fees should be held during this ruleset.
25
25
  /// @custom:member scopeCashOutsToLocalBalances A flag indicating if omnichain cash-out calculations should use only
26
26
  /// the local chain's terminal balance instead of the project's balance held in all terminals.
27
- /// @custom:member pauseCrossProjectFeeFreeInflows If `true`, the project cannot be targeted by
28
- /// `payAfterCashOutTokensOf` / `addToBalanceAfterCashOutTokensOf` calls during this ruleset.
29
27
  /// @custom:member useDataHookForCashOuts A flag indicating if the data hook should be used for cash out transactions
30
28
  /// during
31
29
  /// this ruleset.
@@ -46,7 +44,6 @@ struct JBPayDataHookRulesetMetadata {
46
44
  bool ownerMustSendPayouts;
47
45
  bool holdFees;
48
46
  bool scopeCashOutsToLocalBalances;
49
- bool pauseCrossProjectFeeFreeInflows;
50
47
  bool useDataHookForCashOut;
51
48
  uint16 metadata;
52
49
  }
@@ -162,7 +162,7 @@ contract ForTest_JB721TiersHookStore is JB721TiersHookStore, IJB721TiersHookStor
162
162
  votingUnits: storedTier.price,
163
163
  reserveFrequency: storedTier.reserveFrequency,
164
164
  reserveBeneficiary: reserveBeneficiaryOf(nft, currentSortIndex),
165
- encodedIPFSUri: encodedIPFSUriOf[nft][currentSortIndex],
165
+ encodedIpfsUri: encodedIpfsUriOf[nft][currentSortIndex],
166
166
  category: storedTier.category,
167
167
  discountPercent: storedTier.discountPercent,
168
168
  flags: JB721TierFlags({