@bananapus/721-hook-v6 0.0.45 → 0.0.47

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.
package/CHANGELOG.md CHANGED
@@ -20,7 +20,7 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
20
20
  - The repo now carries a dedicated helper library to keep the hook surface manageable and to support the larger v6 feature set.
21
21
  - The repo was upgraded from the v5 Solidity baseline to `0.8.28`.
22
22
 
23
- ## Local audit remediations
23
+ ## Local review remediations
24
24
 
25
25
  - `JB721TiersHookProjectDeployer.launchRulesetsFor` now checks `LAUNCH_RULESETS` instead of `QUEUE_RULESETS`. The previous check was semantically wrong — launching active rulesets should require the launch permission, not the queue permission.
26
26
 
@@ -43,7 +43,7 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
43
43
 
44
44
  ## Indexer impact
45
45
 
46
- - New events: `AddToBalanceReverted` (declared but no longer emitted -- replaced by `JB721TiersHookLib_SplitFallbackFailed` revert error), `SetName`, `SetSymbol`, `SplitPayoutReverted`.
46
+ - New events: `SetName`, `SetSymbol`, `SplitPayoutReverted`.
47
47
  - Tier config decoding changed because `JB721TierConfig` is no longer v5-compatible.
48
48
  - Collection metadata can now change after deployment, so one-time indexing of `name` and `symbol` is no longer sufficient.
49
49
 
@@ -62,7 +62,6 @@ This file describes the verified change from `nana-721-hook-v5` to the current `
62
62
  - `pricingContext()`
63
63
  - `setMetadata(...)`
64
64
  - Added events
65
- - `AddToBalanceReverted` (declared in interface but no longer emitted; the library now reverts with `JB721TiersHookLib_SplitFallbackFailed` instead)
66
65
  - `SetName`
67
66
  - `SetSymbol`
68
67
  - `SplitPayoutReverted`
package/README.md CHANGED
@@ -73,7 +73,7 @@ That split is why UI bugs, economic bugs, and deployment bugs often land in diff
73
73
  1. `test/E2E/Pay_Mint_Redeem_E2E.t.sol`
74
74
  2. `test/invariants/TierLifecycleInvariant.t.sol`
75
75
  3. `test/invariants/TieredHookStoreInvariant.t.sol`
76
- 4. `test/audit/CodexSplitCreditsMismatch.t.sol`
76
+ 4. `test/regression/RegressionSplitCreditsMismatch.t.sol`
77
77
  5. `test/regression/ProjectDeployerRulesets.t.sol`
78
78
 
79
79
  ## Install
@@ -112,7 +112,7 @@ src/
112
112
  libraries/
113
113
  structs/
114
114
  test/
115
- unit, E2E, fork, invariant, audit, and regression coverage
115
+ unit, E2E, fork, invariant, review, and regression coverage
116
116
  script/
117
117
  Deploy.s.sol
118
118
  helpers/
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.45",
3
+ "version": "0.0.47",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@bananapus/address-registry-v6": "0.0.25",
32
- "@bananapus/core-v6": "^0.0.39",
32
+ "@bananapus/core-v6": "0.0.44",
33
33
  "@bananapus/ownable-v6": "^0.0.24",
34
34
  "@bananapus/permission-ids-v6": "0.0.22",
35
35
  "@openzeppelin/contracts": "5.6.1",
@@ -25,8 +25,8 @@
25
25
  ## Useful Proof Points
26
26
 
27
27
  - [`test/Fork.t.sol`](../test/Fork.t.sol) for live-integration assumptions.
28
- - [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for known edge cases the repo authors considered worth pinning down.
28
+ - [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for known edge cases the repo authors considered worth pinning down.
29
29
  - [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) when you need a narrow function-level proof before editing a broad runtime path.
30
30
  - [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) when a local patch may have broken store-level relationships.
31
- - [`test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol`](../test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol) when reserve-beneficiary or pending-reserve behavior changes.
31
+ - [`test/regression/RetroactiveReserveBeneficiaryDilution.t.sol`](../test/regression/RetroactiveReserveBeneficiaryDilution.t.sol) when reserve-beneficiary or pending-reserve behavior changes.
32
32
  - [`script/Deploy.s.sol`](../script/Deploy.s.sol) when a deployment or launch question is really about config assembly rather than contract behavior.
@@ -30,4 +30,4 @@
30
30
  - [`test/TestVotingUnitsLifecycle.t.sol`](../test/TestVotingUnitsLifecycle.t.sol) for voting-unit lifecycle behavior.
31
31
  - [`test/TestCheckpoints.t.sol`](../test/TestCheckpoints.t.sol) for checkpoint/module behavior.
32
32
  - [`test/invariants/TierLifecycleInvariant.t.sol`](../test/invariants/TierLifecycleInvariant.t.sol) and [`test/invariants/TieredHookStoreInvariant.t.sol`](../test/invariants/TieredHookStoreInvariant.t.sol) for store-level lifecycle invariants.
33
- - [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol), [`test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol`](../test/audit/CodexRetroactiveReserveBeneficiaryDilution.t.sol), and [`test/TestAuditGaps.sol`](../test/TestAuditGaps.sol) for reentrancy and attack-surface checks.
33
+ - [`test/TestSafeTransferReentrancy.t.sol`](../test/TestSafeTransferReentrancy.t.sol), [`test/721HookAttacks.t.sol`](../test/721HookAttacks.t.sol), [`test/regression/RetroactiveReserveBeneficiaryDilution.t.sol`](../test/regression/RetroactiveReserveBeneficiaryDilution.t.sol), and [`test/TestRegressionGaps.sol`](../test/TestRegressionGaps.sol) for reentrancy and attack-surface checks.
@@ -35,7 +35,7 @@ library Hook721DeploymentLib {
35
35
 
36
36
  for (uint256 _i; _i < networks.length; _i++) {
37
37
  if (networks[_i].chainId == chainId) {
38
- return getDeployment(path, networks[_i].name);
38
+ return getDeployment({path: path, network_name: networks[_i].name});
39
39
  }
40
40
  }
41
41
 
@@ -52,15 +52,31 @@ library Hook721DeploymentLib {
52
52
  returns (Hook721Deployment memory deployment)
53
53
  {
54
54
  deployment.hook_deployer = IJB721TiersHookDeployer(
55
- _getDeploymentAddress(path, "nana-721-hook-v6", network_name, "JB721TiersHookDeployer")
55
+ _getDeploymentAddress({
56
+ path: path,
57
+ project_name: "nana-721-hook-v6",
58
+ network_name: network_name,
59
+ contractName: "JB721TiersHookDeployer"
60
+ })
56
61
  );
57
62
 
58
63
  deployment.project_deployer = IJB721TiersHookProjectDeployer(
59
- _getDeploymentAddress(path, "nana-721-hook-v6", network_name, "JB721TiersHookProjectDeployer")
64
+ _getDeploymentAddress({
65
+ path: path,
66
+ project_name: "nana-721-hook-v6",
67
+ network_name: network_name,
68
+ contractName: "JB721TiersHookProjectDeployer"
69
+ })
60
70
  );
61
71
 
62
- deployment.store =
63
- IJB721TiersHookStore(_getDeploymentAddress(path, "nana-721-hook-v6", network_name, "JB721TiersHookStore"));
72
+ deployment.store = IJB721TiersHookStore(
73
+ _getDeploymentAddress({
74
+ path: path,
75
+ project_name: "nana-721-hook-v6",
76
+ network_name: network_name,
77
+ contractName: "JB721TiersHookStore"
78
+ })
79
+ );
64
80
  }
65
81
 
66
82
  /// @notice Get the address of a contract that was deployed by the Deploy script.
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.28;
3
3
 
4
+ import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol";
4
5
  import {Votes} from "@openzeppelin/contracts/governance/utils/Votes.sol";
5
6
  import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
6
7
  import {Checkpoints} from "@openzeppelin/contracts/utils/structs/Checkpoints.sol";
@@ -23,8 +24,9 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
23
24
  // --------------------------- custom errors ------------------------- //
24
25
  //*********************************************************************//
25
26
 
26
- error JB721Checkpoints_AlreadyInitialized();
27
- error JB721Checkpoints_Unauthorized();
27
+ error JB721Checkpoints_AlreadyInitialized(address hook);
28
+ error JB721Checkpoints_NotOwner(uint256 tokenId, address caller);
29
+ error JB721Checkpoints_Unauthorized(address caller, address hook);
28
30
 
29
31
  //*********************************************************************//
30
32
  // --------------- public immutable stored properties ---------------- //
@@ -44,7 +46,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
44
46
  // -------------------- internal stored properties ------------------- //
45
47
  //*********************************************************************//
46
48
 
47
- /// @notice Checkpointed token owners for historical reward eligibility after first transfer.
49
+ /// @notice Checkpointed token owners for historical reward eligibility. Written on enrollment or transfer.
48
50
  /// @custom:param tokenId The token ID to get historical owner checkpoints for.
49
51
  mapping(uint256 tokenId => Checkpoints.Trace160) internal _ownerCheckpointsOf;
50
52
 
@@ -64,13 +66,43 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
64
66
  // ---------------------- external transactions ---------------------- //
65
67
  //*********************************************************************//
66
68
 
69
+ /// @notice Delegates voting power and enrolls tokens for distribution eligibility.
70
+ /// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
71
+ /// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
72
+ /// distribution. The existing `delegate(address)` from OZ Votes still works for pure delegation without enrollment.
73
+ /// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
74
+ /// @param tokenIds The token IDs to enroll for distribution eligibility.
75
+ function delegate(address delegatee, uint256[] calldata tokenIds) external override {
76
+ // Delegate voting power (reuses OZ Votes internals).
77
+ _delegate({account: msg.sender, delegatee: delegatee});
78
+
79
+ // Write per-token owner checkpoints for distribution eligibility.
80
+ for (uint256 i; i < tokenIds.length;) {
81
+ uint256 tokenId = tokenIds[i];
82
+
83
+ // Only the current owner can enroll their tokens.
84
+ if (IERC721(HOOK).ownerOf(tokenId) != msg.sender) {
85
+ revert JB721Checkpoints_NotOwner({tokenId: tokenId, caller: msg.sender});
86
+ }
87
+
88
+ // Write an owner checkpoint if the token has none yet.
89
+ if (_ownerCheckpointsOf[tokenId].length() == 0) {
90
+ // forge-lint: disable-next-line(unsafe-typecast)
91
+ _ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(msg.sender)});
92
+ }
93
+
94
+ unchecked {
95
+ ++i;
96
+ }
97
+ }
98
+ }
99
+
67
100
  /// @notice Initializes a cloned module with its hook reference.
68
101
  /// @dev Can only be called once. Called by the deployer after cloning.
69
102
  /// @param hook The hook this module serves.
70
103
  function initialize(address hook) external override {
71
- if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized();
104
+ if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized({hook: HOOK});
72
105
  // `hook` cannot be zero when called through the deployer because `msg.sender` must equal `hook`.
73
- // slither-disable-next-line missing-zero-check
74
106
  HOOK = hook;
75
107
  }
76
108
 
@@ -80,11 +112,10 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
80
112
  /// @param to The new owner (address(0) on burn).
81
113
  /// @param tokenId The token ID to transfer.
82
114
  function onTransfer(address from, address to, uint256 tokenId) external override {
83
- if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized();
115
+ if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized({caller: msg.sender, hook: HOOK});
84
116
 
85
117
  if (from != address(0)) {
86
118
  // forge-lint: disable-next-line(unsafe-typecast)
87
- // slither-disable-next-line unused-return
88
119
  _ownerCheckpointsOf[tokenId].push({key: uint96(block.number), value: uint160(to)});
89
120
  }
90
121
 
@@ -100,11 +131,11 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
100
131
  //*********************************************************************//
101
132
 
102
133
  /// @notice The owner of an NFT at a past block.
103
- /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
104
- /// inferred from the hook's `firstOwnerOf`.
134
+ /// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
135
+ /// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
105
136
  /// @param tokenId The token ID of the NFT to get the historical owner of.
106
137
  /// @param blockNumber The block number to look up.
107
- /// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
138
+ /// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
108
139
  function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view override returns (address) {
109
140
  // forge-lint: disable-next-line(unsafe-typecast)
110
141
  uint96 blockNumber96 = uint96(blockNumber);
@@ -112,10 +143,11 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
112
143
  Checkpoints.Trace160 storage checkpoints = _ownerCheckpointsOf[tokenId];
113
144
  uint256 checkpointCount = checkpoints.length();
114
145
 
115
- // Before the first transfer/burn checkpoint, the mint owner is implicit in the hook's first-owner tracking.
116
- if (checkpointCount == 0 || checkpoints.at(0)._key > blockNumber96) {
117
- return IJB721TiersHook(HOOK).firstOwnerOf(tokenId);
118
- }
146
+ // No checkpoints = not enrolled and never transferred. Not eligible.
147
+ if (checkpointCount == 0) return address(0);
148
+
149
+ // Query is before the first checkpoint — token not yet enrolled/transferred at this block.
150
+ if (checkpoints.at(0)._key > blockNumber96) return address(0);
119
151
 
120
152
  return address(uint160(checkpoints.upperLookupRecent(blockNumber96)));
121
153
  }
@@ -13,6 +13,12 @@ import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
13
13
  /// @dev The implementation is deployed once in the constructor. Each `deploy()` call clones it (~45k gas) and
14
14
  /// initializes the clone with the hook and store references.
15
15
  contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
16
+ //*********************************************************************//
17
+ // --------------------------- custom errors ------------------------- //
18
+ //*********************************************************************//
19
+
20
+ error JB721CheckpointsDeployer_Unauthorized(address caller, address hook);
21
+
16
22
  //*********************************************************************//
17
23
  // --------------- public immutable stored properties ---------------- //
18
24
  //*********************************************************************//
@@ -42,7 +48,9 @@ contract JB721CheckpointsDeployer is IJB721CheckpointsDeployer {
42
48
  /// @param hook The hook address the module will serve.
43
49
  /// @return module The newly deployed and initialized checkpoint module.
44
50
  function deploy(address hook) external override returns (IJB721Checkpoints module) {
45
- if (msg.sender != hook) revert JB721CheckpointsDeployer_Unauthorized();
51
+ if (msg.sender != hook) {
52
+ revert JB721CheckpointsDeployer_Unauthorized({caller: msg.sender, hook: hook});
53
+ }
46
54
 
47
55
  module = IJB721Checkpoints(
48
56
  LibClone.cloneDeterministic({implementation: IMPLEMENTATION, salt: bytes32(uint256(uint160(hook)))})
@@ -42,12 +42,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
42
42
  //*********************************************************************//
43
43
 
44
44
  error JB721TiersHook_AlreadyInitialized(uint256 projectId);
45
- error JB721TiersHook_CantBuyWithCredits();
46
45
  error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
47
- error JB721TiersHook_MintReserveNftsPaused();
48
- error JB721TiersHook_NoProjectId();
49
- error JB721TiersHook_Overspending(uint256 leftoverAmount);
50
- error JB721TiersHook_TierTransfersPaused();
46
+ error JB721TiersHook_MintReserveNftsPaused(uint256 projectId, uint256 tierId);
47
+ error JB721TiersHook_NoProjectId(uint256 projectId);
48
+ error JB721TiersHook_TierTransfersPaused(uint256 projectId, uint256 tokenId, address from, address to);
51
49
 
52
50
  //*********************************************************************//
53
51
  // --------------- public immutable stored properties ---------------- //
@@ -264,7 +262,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
264
262
  _initialized = true;
265
263
 
266
264
  // Make sure a projectId is provided.
267
- if (projectId == 0) revert JB721TiersHook_NoProjectId();
265
+ if (projectId == 0) revert JB721TiersHook_NoProjectId({projectId: projectId});
268
266
 
269
267
  // Initialize the superclass.
270
268
  JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
@@ -279,7 +277,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
279
277
  // pack the pricing decimals in bits 32-39 (8 bits).
280
278
  packed |= uint256(tiersConfig.decimals) << 32;
281
279
  // Store the packed value.
282
- // slither-disable-next-line events-maths
283
280
  _packedPricingContext = packed;
284
281
 
285
282
  // Store the base URI if provided.
@@ -385,7 +382,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
385
382
  _requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.MINT_721});
386
383
 
387
384
  // Record the mint. The token IDs returned correspond to the tiers passed in.
388
- // slither-disable-next-line reentrancy-events,unused-return
389
385
  (tokenIds,,) = STORE.recordMint({
390
386
  amount: type(uint256).max, // force the mint.
391
387
  tierIds: tierIds,
@@ -503,7 +499,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
503
499
  // `address(this)` is the sentinel value meaning "leave unchanged" (since `address(0)` clears the resolver).
504
500
  if (tokenUriResolver != IJB721TokenUriResolver(address(this))) {
505
501
  // Store the new URI resolver.
506
- // slither-disable-next-line reentrancy-events
507
502
  _recordSetTokenUriResolver(tokenUriResolver);
508
503
  }
509
504
  if (encodedIPFSUriTierId != 0 && encodedIPFSUri != bytes32(0)) {
@@ -529,15 +524,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
529
524
  // Pending reserve mints must not be paused.
530
525
  if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused((JBRulesetMetadataResolver.metadata(ruleset))))
531
526
  {
532
- revert JB721TiersHook_MintReserveNftsPaused();
527
+ revert JB721TiersHook_MintReserveNftsPaused({projectId: PROJECT_ID, tierId: tierId});
533
528
  }
534
529
 
535
530
  // Record the reserved mint for the tier.
536
- // slither-disable-next-line reentrancy-events,calls-loop
537
531
  uint256[] memory tokenIds = STORE.recordMintReservesFor({tierId: tierId, count: count});
538
532
 
539
533
  // Keep a reference to the beneficiary.
540
- // slither-disable-next-line calls-loop
541
534
  address reserveBeneficiary = STORE.reserveBeneficiaryOf({hook: address(this), tierId: tierId});
542
535
 
543
536
  // Cache _msgSender() before the loop to avoid repeated calls.
@@ -550,7 +543,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
550
543
  emit MintReservedNft({tokenId: tokenId, tierId: tierId, beneficiary: reserveBeneficiary, caller: caller});
551
544
 
552
545
  // Mint the NFT.
553
- // slither-disable-next-line reentrency-events
554
546
  _mint({to: reserveBeneficiary, tokenId: tokenId});
555
547
 
556
548
  unchecked {
@@ -572,7 +564,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
572
564
  /// @param projectId The ID of the project to check.
573
565
  /// @return The project's current ruleset.
574
566
  function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) {
575
- // slither-disable-next-line calls-loop
576
567
  return RULESETS.currentOf(projectId);
577
568
  }
578
569
 
@@ -625,7 +616,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
625
616
  caller: caller
626
617
  });
627
618
 
628
- // slither-disable-next-line reentrancy-events
629
619
  _mint({to: beneficiary, tokenId: tokenIds[i]});
630
620
 
631
621
  unchecked {
@@ -665,7 +655,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
665
655
  if (tokenIds.length != 0) {
666
656
  // totalAmountPaid is the full amount available before recordMint deducted tier prices.
667
657
  uint256 totalAmountPaid = (payer == beneficiary) ? value + payCredits : value;
668
- // slither-disable-next-line reentrancy-events
669
658
  _mintTokens({
670
659
  tokenIds: tokenIds, tierIds: tierIdsToMint, beneficiary: beneficiary, totalAmountPaid: totalAmountPaid
671
660
  });
@@ -689,7 +678,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
689
678
  });
690
679
  }
691
680
 
692
- // slither-disable-next-line reentrancy-no-eth
693
681
  payCreditsOf[beneficiary] = newPayCredits;
694
682
  }
695
683
  }
@@ -757,7 +745,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
757
745
  /// @param tierId The ID of the tier to set the discount percent for.
758
746
  /// @param discountPercent The discount percent to set (0 = no discount, up to DISCOUNT_DENOMINATOR = free).
759
747
  function _setDiscountPercentOf(uint256 tierId, uint256 discountPercent) internal {
760
- // slither-disable-next-line calls-loop
761
748
  JB721TiersHookLib.setDiscountPercentOf({
762
749
  store: STORE, tierId: tierId, discountPercent: discountPercent, caller: _msgSender()
763
750
  });
@@ -768,7 +755,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
768
755
  /// @param tokenId The token ID of the NFT to transfer.
769
756
  function _update(address to, uint256 tokenId, address auth) internal virtual override returns (address from) {
770
757
  // Get only the tier ID and transfersPausable flag (lightweight — avoids full struct construction).
771
- // slither-disable-next-line calls-loop
772
758
  (uint256 tierId, bool transfersPausable) =
773
759
  STORE.tierTransferInfoOfTokenId({hook: address(this), tokenId: tokenId});
774
760
 
@@ -788,26 +774,26 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
788
774
  && JB721TiersRulesetMetadataResolver.transfersPaused(
789
775
  (JBRulesetMetadataResolver.metadata(ruleset))
790
776
  )
791
- ) revert JB721TiersHook_TierTransfersPaused();
777
+ ) {
778
+ revert JB721TiersHook_TierTransfersPaused({
779
+ projectId: PROJECT_ID, tokenId: tokenId, from: from, to: to
780
+ });
781
+ }
792
782
  }
793
783
 
794
784
  // If the token isn't already associated with a first owner, store the sender as the first owner.
795
- // slither-disable-next-line calls-loop
796
785
  if (_firstOwnerOf[tokenId] == address(0)) _firstOwnerOf[tokenId] = from;
797
786
  }
798
787
 
799
788
  // Record the transfer.
800
- // slither-disable-next-line reentrency-events,calls-loop
801
789
  STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
802
790
 
803
791
  // Deploy the checkpoint module lazily on the first transfer.
804
792
  if (address(CHECKPOINTS) == address(0)) {
805
- // slither-disable-next-line calls-loop,reentrancy-events
806
793
  CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy(address(this));
807
794
  }
808
795
 
809
796
  // Notify the checkpoint module to update checkpointed voting power.
810
- // slither-disable-next-line calls-loop,reentrancy-events
811
797
  CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
812
798
  }
813
799
  }
@@ -103,7 +103,6 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
103
103
  JBOwnable(address(newHook)).transferOwnership(_msgSender());
104
104
 
105
105
  // Increment the nonce.
106
- // slither-disable-next-line reentrancy-benign
107
106
  ++_nonce;
108
107
 
109
108
  // Add the hook to the address registry. This contract's nonce starts at 1.
@@ -276,7 +276,7 @@ contract JB721TiersHookProjectDeployer is
276
276
  allowAddPriceFeed: payDataRulesetConfig.metadata.allowAddPriceFeed,
277
277
  ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts,
278
278
  holdFees: payDataRulesetConfig.metadata.holdFees,
279
- useTotalSurplusForCashOuts: payDataRulesetConfig.metadata.useTotalSurplusForCashOuts,
279
+ scopeCashOutsToLocalBalances: payDataRulesetConfig.metadata.scopeCashOutsToLocalBalances,
280
280
  useDataHookForPay: true,
281
281
  useDataHookForCashOut: payDataRulesetConfig.metadata.useDataHookForCashOut,
282
282
  dataHook: address(dataHook),
@@ -292,7 +292,6 @@ contract JB721TiersHookProjectDeployer is
292
292
  }
293
293
 
294
294
  // Launch the rulesets for the reserved project.
295
- // slither-disable-next-line unused-return
296
295
  controller.launchRulesetsFor({
297
296
  projectId: projectId,
298
297
  projectUri: launchProjectConfig.projectUri,
@@ -351,7 +350,7 @@ contract JB721TiersHookProjectDeployer is
351
350
  allowAddPriceFeed: payDataRulesetConfig.metadata.allowAddPriceFeed,
352
351
  ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts,
353
352
  holdFees: payDataRulesetConfig.metadata.holdFees,
354
- useTotalSurplusForCashOuts: payDataRulesetConfig.metadata.useTotalSurplusForCashOuts,
353
+ scopeCashOutsToLocalBalances: payDataRulesetConfig.metadata.scopeCashOutsToLocalBalances,
355
354
  useDataHookForPay: true,
356
355
  useDataHookForCashOut: payDataRulesetConfig.metadata.useDataHookForCashOut,
357
356
  dataHook: address(dataHook),
@@ -426,7 +425,7 @@ contract JB721TiersHookProjectDeployer is
426
425
  allowAddPriceFeed: payDataRulesetConfig.metadata.allowAddPriceFeed,
427
426
  ownerMustSendPayouts: payDataRulesetConfig.metadata.ownerMustSendPayouts,
428
427
  holdFees: payDataRulesetConfig.metadata.holdFees,
429
- useTotalSurplusForCashOuts: payDataRulesetConfig.metadata.useTotalSurplusForCashOuts,
428
+ scopeCashOutsToLocalBalances: payDataRulesetConfig.metadata.scopeCashOutsToLocalBalances,
430
429
  useDataHookForPay: true,
431
430
  useDataHookForCashOut: payDataRulesetConfig.metadata.useDataHookForCashOut,
432
431
  dataHook: address(dataHook),
@@ -31,7 +31,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
31
31
 
32
32
  error JB721TiersHookStore_CantMintManually(uint256 tierId);
33
33
  error JB721TiersHookStore_CantRemoveTier(uint256 tierId);
34
- error JB721TiersHookStore_DeadlockedReserve();
34
+ error JB721TiersHookStore_DeadlockedReserve(uint256 tierId, uint256 initialSupply, uint256 reserveFrequency);
35
35
  error JB721TiersHookStore_DiscountPercentExceedsBounds(uint256 percent, uint256 limit);
36
36
  error JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(uint256 percent, uint256 storedPercent);
37
37
  error JB721TiersHookStore_InsufficientPendingReserves(uint256 count, uint256 numberOfPendingReserves);
@@ -618,7 +618,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
618
618
  // Cache packed bools to avoid stack-too-deep from destructuring all 6 bools.
619
619
  uint8 packed = storedTier.packedBools;
620
620
 
621
- // slither-disable-next-line calls-loop
622
621
  return JB721Tier({
623
622
  // forge-lint: disable-next-line(unsafe-typecast)
624
623
  id: uint32(tierId),
@@ -838,11 +837,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
838
837
  uint256 currentSortedTierId = _firstSortedTierIdOf({hook: hook, category: 0});
839
838
 
840
839
  // Keep track of the previous non-removed tier ID.
841
- // slither-disable-next-line uninitialized-local
842
840
  uint256 previousSortedTierId;
843
841
 
844
842
  // Initialize a `JBBitmapWord` for tracking removed tiers.
845
- // slither-disable-next-line uninitialized-local
846
843
  JBBitmapWord memory bitmapWord;
847
844
 
848
845
  // Make the sorted array.
@@ -905,7 +902,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
905
902
 
906
903
  // Make sure the max number of tiers won't be exceeded.
907
904
  if (currentMaxTierIdOf + tiersToAdd.length > type(uint16).max) {
908
- revert JB721TiersHookStore_MaxTiersExceeded(currentMaxTierIdOf + tiersToAdd.length, type(uint16).max);
905
+ revert JB721TiersHookStore_MaxTiersExceeded({
906
+ numberOfTiers: currentMaxTierIdOf + tiersToAdd.length, limit: type(uint16).max
907
+ });
909
908
  }
910
909
 
911
910
  // Keep a reference to the current last sorted tier ID (sorted by category).
@@ -919,7 +918,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
919
918
  uint256 startSortedTierId = currentMaxTierIdOf == 0 ? 0 : _firstSortedTierIdOf({hook: msg.sender, category: 0});
920
919
 
921
920
  // Keep track of the previous tier's ID while iterating.
922
- // slither-disable-next-line uninitialized-local
923
921
  uint256 previousTierId;
924
922
 
925
923
  // Keep a reference to the 721 contract's flags.
@@ -932,7 +930,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
932
930
  // Make sure the supply maximum is enforced. If it's greater than one billion, it would overflow into the
933
931
  // next tier.
934
932
  if (tierToAdd.initialSupply > _ONE_BILLION - 1) {
935
- revert JB721TiersHookStore_InvalidQuantity(tierToAdd.initialSupply, _ONE_BILLION - 1);
933
+ revert JB721TiersHookStore_InvalidQuantity({quantity: tierToAdd.initialSupply, limit: _ONE_BILLION - 1});
936
934
  }
937
935
 
938
936
  // Make sure the tier's category is greater than or equal to the previously added tier's category.
@@ -942,7 +940,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
942
940
 
943
941
  // Revert if the category is not equal or greater than the previously added tier's category.
944
942
  if (tierToAdd.category < previousCategory) {
945
- revert JB721TiersHookStore_InvalidCategorySortOrder(tierToAdd.category, previousCategory);
943
+ revert JB721TiersHookStore_InvalidCategorySortOrder({
944
+ tierCategory: tierToAdd.category, previousTierCategory: previousCategory
945
+ });
946
946
  }
947
947
  }
948
948
 
@@ -955,32 +955,32 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
955
955
  && ((tierToAdd.flags.useVotingUnits && tierToAdd.votingUnits != 0)
956
956
  || (!tierToAdd.flags.useVotingUnits && tierToAdd.price != 0))
957
957
  ) {
958
- revert JB721TiersHookStore_VotingUnitsNotAllowed(tierId);
958
+ revert JB721TiersHookStore_VotingUnitsNotAllowed({tierId: tierId});
959
959
  }
960
960
 
961
961
  // Make sure the new tier doesn't have a reserve frequency if the 721 contract's flags don't allow it to,
962
962
  // OR if manual minting is allowed.
963
963
  if ((flags.noNewTiersWithReserves || tierToAdd.flags.allowOwnerMint) && tierToAdd.reserveFrequency != 0) {
964
- revert JB721TiersHookStore_ReserveFrequencyNotAllowed(tierId);
964
+ revert JB721TiersHookStore_ReserveFrequencyNotAllowed({tierId: tierId});
965
965
  }
966
966
 
967
967
  // Make sure the new tier doesn't have owner minting enabled if the 721 contract's flags don't allow it to.
968
968
  if (flags.noNewTiersWithOwnerMinting && tierToAdd.flags.allowOwnerMint) {
969
- revert JB721TiersHookStore_ManualMintingNotAllowed(tierId);
969
+ revert JB721TiersHookStore_ManualMintingNotAllowed({tierId: tierId});
970
970
  }
971
971
 
972
972
  // Make sure the discount percent is within the bound.
973
973
  if (tierToAdd.discountPercent > JB721Constants.DISCOUNT_DENOMINATOR) {
974
- revert JB721TiersHookStore_DiscountPercentExceedsBounds(
975
- tierToAdd.discountPercent, JB721Constants.DISCOUNT_DENOMINATOR
976
- );
974
+ revert JB721TiersHookStore_DiscountPercentExceedsBounds({
975
+ percent: tierToAdd.discountPercent, limit: JB721Constants.DISCOUNT_DENOMINATOR
976
+ });
977
977
  }
978
978
 
979
979
  // Make sure the split percent is within the bound.
980
980
  if (tierToAdd.splitPercent > JBConstants.SPLITS_TOTAL_PERCENT) {
981
- revert JB721TiersHookStore_SplitPercentExceedsBounds(
982
- tierToAdd.splitPercent, JBConstants.SPLITS_TOTAL_PERCENT
983
- );
981
+ revert JB721TiersHookStore_SplitPercentExceedsBounds({
982
+ percent: tierToAdd.splitPercent, limit: JBConstants.SPLITS_TOTAL_PERCENT
983
+ });
984
984
  }
985
985
 
986
986
  // Make sure the tier has a non-zero supply.
@@ -989,7 +989,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
989
989
  // A tier with initialSupply == 1 and reserveFrequency > 0 deadlocks: the single mint is reserved,
990
990
  // leaving zero available for paid mints, but reserves only mint after a paid mint triggers them.
991
991
  if (tierToAdd.initialSupply == 1 && tierToAdd.reserveFrequency > 0) {
992
- revert JB721TiersHookStore_DeadlockedReserve();
992
+ revert JB721TiersHookStore_DeadlockedReserve({
993
+ tierId: tierId, initialSupply: tierToAdd.initialSupply, reserveFrequency: tierToAdd.reserveFrequency
994
+ });
993
995
  }
994
996
 
995
997
  // A tier with reserves must have a beneficiary — either tier-specific or a previously set default.
@@ -998,7 +1000,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
998
1000
  tierToAdd.reserveFrequency > 0 && tierToAdd.reserveBeneficiary == address(0)
999
1001
  && defaultReserveBeneficiaryOf[msg.sender] == address(0)
1000
1002
  ) {
1001
- revert JB721TiersHookStore_MissingReserveBeneficiary(tierId);
1003
+ revert JB721TiersHookStore_MissingReserveBeneficiary({tierId: tierId});
1002
1004
  }
1003
1005
 
1004
1006
  // Store the tier with that ID.
@@ -1213,7 +1215,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1213
1215
 
1214
1216
  // Make sure the tier hasn't been removed.
1215
1217
  if (_isTierRemovedWithRefresh({hook: msg.sender, tierId: tierId, bitmapWord: bitmapWord})) {
1216
- revert JB721TiersHookStore_TierRemoved(tierId);
1218
+ revert JB721TiersHookStore_TierRemoved({tierId: tierId});
1217
1219
  }
1218
1220
 
1219
1221
  // Keep a reference to the stored tier being iterated on.
@@ -1265,7 +1267,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1265
1267
  storedTier.remainingSupply
1266
1268
  < _numberOfPendingReservesFor({hook: msg.sender, tierId: tierId, storedTier: storedTier})
1267
1269
  ) {
1268
- revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
1270
+ revert JB721TiersHookStore_InsufficientSupplyRemaining({tierId: tierId});
1269
1271
  }
1270
1272
 
1271
1273
  unchecked {
@@ -1295,7 +1297,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1295
1297
 
1296
1298
  // Can't mint more than the number of pending reserves.
1297
1299
  if (count > numberOfPendingReserves) {
1298
- revert JB721TiersHookStore_InsufficientPendingReserves(count, numberOfPendingReserves);
1300
+ revert JB721TiersHookStore_InsufficientPendingReserves({
1301
+ count: count, numberOfPendingReserves: numberOfPendingReserves
1302
+ });
1299
1303
  }
1300
1304
 
1301
1305
  // Increment the number of reserve NFTs minted.
@@ -1330,7 +1334,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1330
1334
  // Reject tier IDs that don't exist yet — removing a future tier would cause it
1331
1335
  // to be born already removed when later added.
1332
1336
  if (tierId == 0 || tierId > maxTierIdOf[msg.sender]) {
1333
- revert JB721TiersHookStore_UnrecognizedTier(tierId);
1337
+ revert JB721TiersHookStore_UnrecognizedTier({tierId: tierId});
1334
1338
  }
1335
1339
 
1336
1340
  // Get a reference to the stored tier.
@@ -1363,9 +1367,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1363
1367
 
1364
1368
  // Make sure the discount percent is within the bound.
1365
1369
  if (discountPercent > JB721Constants.DISCOUNT_DENOMINATOR) {
1366
- revert JB721TiersHookStore_DiscountPercentExceedsBounds(
1367
- discountPercent, JB721Constants.DISCOUNT_DENOMINATOR
1368
- );
1370
+ revert JB721TiersHookStore_DiscountPercentExceedsBounds({
1371
+ percent: discountPercent, limit: JB721Constants.DISCOUNT_DENOMINATOR
1372
+ });
1369
1373
  }
1370
1374
 
1371
1375
  // Get a reference to the stored tier.
@@ -1376,7 +1380,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1376
1380
 
1377
1381
  // Make sure that increasing the discount is allowed for the tier.
1378
1382
  if (discountPercent > storedTier.discountPercent && cantIncreaseDiscountPercent) {
1379
- revert JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(discountPercent, storedTier.discountPercent);
1383
+ revert JB721TiersHookStore_DiscountPercentIncreaseNotAllowed({
1384
+ percent: discountPercent, storedPercent: storedTier.discountPercent
1385
+ });
1380
1386
  }
1381
1387
 
1382
1388
  // Set the discount.
@@ -72,7 +72,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
72
72
  */
73
73
  function balanceOf(address owner) public view virtual returns (uint256) {
74
74
  if (owner == address(0)) {
75
- revert ERC721InvalidOwner(address(0));
75
+ revert ERC721InvalidOwner({owner: address(0)});
76
76
  }
77
77
  return _balances[owner];
78
78
  }
@@ -153,13 +153,13 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
153
153
  */
154
154
  function transferFrom(address from, address to, uint256 tokenId) public virtual {
155
155
  if (to == address(0)) {
156
- revert ERC721InvalidReceiver(address(0));
156
+ revert ERC721InvalidReceiver({receiver: address(0)});
157
157
  }
158
158
  // Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists
159
159
  // (from != 0). Therefore, it is not needed to verify that the return value is not 0 here.
160
160
  address previousOwner = _update({to: to, tokenId: tokenId, auth: _msgSender()});
161
161
  if (previousOwner != from) {
162
- revert ERC721IncorrectOwner(from, tokenId, previousOwner);
162
+ revert ERC721IncorrectOwner({sender: from, tokenId: tokenId, owner: previousOwner});
163
163
  }
164
164
  }
165
165
 
@@ -206,7 +206,9 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
206
206
  */
207
207
  function _isAuthorized(address owner, address spender, uint256 tokenId) internal view virtual returns (bool) {
208
208
  return spender != address(0)
209
- && (owner == spender || isApprovedForAll(owner, spender) || _getApproved(tokenId) == spender);
209
+ && (owner == spender
210
+ || isApprovedForAll({owner: owner, operator: spender})
211
+ || _getApproved(tokenId) == spender);
210
212
  }
211
213
 
212
214
  /**
@@ -218,11 +220,11 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
218
220
  * assumption.
219
221
  */
220
222
  function _checkAuthorized(address owner, address spender, uint256 tokenId) internal view virtual {
221
- if (!_isAuthorized(owner, spender, tokenId)) {
223
+ if (!_isAuthorized({owner: owner, spender: spender, tokenId: tokenId})) {
222
224
  if (owner == address(0)) {
223
- revert ERC721NonexistentToken(tokenId);
225
+ revert ERC721NonexistentToken({tokenId: tokenId});
224
226
  } else {
225
- revert ERC721InsufficientApproval(spender, tokenId);
227
+ revert ERC721InsufficientApproval({operator: spender, tokenId: tokenId});
226
228
  }
227
229
  }
228
230
  }
@@ -280,7 +282,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
280
282
 
281
283
  _owners[tokenId] = to;
282
284
 
283
- emit Transfer(from, to, tokenId);
285
+ emit Transfer({from: from, to: to, tokenId: tokenId});
284
286
 
285
287
  return from;
286
288
  }
@@ -299,11 +301,11 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
299
301
  */
300
302
  function _mint(address to, uint256 tokenId) internal {
301
303
  if (to == address(0)) {
302
- revert ERC721InvalidReceiver(address(0));
304
+ revert ERC721InvalidReceiver({receiver: address(0)});
303
305
  }
304
306
  address previousOwner = _update({to: to, tokenId: tokenId, auth: address(0)});
305
307
  if (previousOwner != address(0)) {
306
- revert ERC721InvalidSender(address(0));
308
+ revert ERC721InvalidSender({sender: address(0)});
307
309
  }
308
310
  }
309
311
 
@@ -345,7 +347,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
345
347
  function _burn(uint256 tokenId) internal {
346
348
  address previousOwner = _update({to: address(0), tokenId: tokenId, auth: address(0)});
347
349
  if (previousOwner == address(0)) {
348
- revert ERC721NonexistentToken(tokenId);
350
+ revert ERC721NonexistentToken({tokenId: tokenId});
349
351
  }
350
352
  }
351
353
 
@@ -362,13 +364,13 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
362
364
  */
363
365
  function _transfer(address from, address to, uint256 tokenId) internal {
364
366
  if (to == address(0)) {
365
- revert ERC721InvalidReceiver(address(0));
367
+ revert ERC721InvalidReceiver({receiver: address(0)});
366
368
  }
367
369
  address previousOwner = _update({to: to, tokenId: tokenId, auth: address(0)});
368
370
  if (previousOwner == address(0)) {
369
- revert ERC721NonexistentToken(tokenId);
371
+ revert ERC721NonexistentToken({tokenId: tokenId});
370
372
  } else if (previousOwner != from) {
371
- revert ERC721IncorrectOwner(from, tokenId, previousOwner);
373
+ revert ERC721IncorrectOwner({sender: from, tokenId: tokenId, owner: previousOwner});
372
374
  }
373
375
  }
374
376
 
@@ -430,12 +432,12 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
430
432
  address owner = _requireOwned(tokenId);
431
433
 
432
434
  // We do not use _isAuthorized because single-token approvals should not be able to call approve
433
- if (auth != address(0) && owner != auth && !isApprovedForAll(owner, auth)) {
434
- revert ERC721InvalidApprover(auth);
435
+ if (auth != address(0) && owner != auth && !isApprovedForAll({owner: owner, operator: auth})) {
436
+ revert ERC721InvalidApprover({approver: auth});
435
437
  }
436
438
 
437
439
  if (emitEvent) {
438
- emit Approval(owner, to, tokenId);
440
+ emit Approval({owner: owner, approved: to, tokenId: tokenId});
439
441
  }
440
442
  }
441
443
 
@@ -452,10 +454,10 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
452
454
  */
453
455
  function _setApprovalForAll(address owner, address operator, bool approved) internal virtual {
454
456
  if (operator == address(0)) {
455
- revert ERC721InvalidOperator(operator);
457
+ revert ERC721InvalidOperator({operator: operator});
456
458
  }
457
459
  _operatorApprovals[owner][operator] = approved;
458
- emit ApprovalForAll(owner, operator, approved);
460
+ emit ApprovalForAll({owner: owner, operator: operator, approved: approved});
459
461
  }
460
462
 
461
463
  /**
@@ -467,7 +469,7 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
467
469
  function _requireOwned(uint256 tokenId) internal view returns (address) {
468
470
  address owner = _ownerOf(tokenId);
469
471
  if (owner == address(0)) {
470
- revert ERC721NonexistentToken(tokenId);
472
+ revert ERC721NonexistentToken({tokenId: tokenId});
471
473
  }
472
474
  return owner;
473
475
  }
@@ -489,11 +491,11 @@ abstract contract ERC721 is Context, ERC165, IERC721, IERC721Metadata, IERC721Er
489
491
  bytes4 retval
490
492
  ) {
491
493
  if (retval != IERC721Receiver.onERC721Received.selector) {
492
- revert ERC721InvalidReceiver(to);
494
+ revert ERC721InvalidReceiver({receiver: to});
493
495
  }
494
496
  } catch (bytes memory reason) {
495
497
  if (reason.length == 0) {
496
- revert ERC721InvalidReceiver(to);
498
+ revert ERC721InvalidReceiver({receiver: to});
497
499
  } else {
498
500
  /// @solidity memory-safe-assembly
499
501
  assembly {
@@ -30,10 +30,10 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
30
30
  // --------------------------- custom errors ------------------------- //
31
31
  //*********************************************************************//
32
32
 
33
- error JB721Hook_InvalidCashOut();
34
- error JB721Hook_InvalidPay();
33
+ error JB721Hook_InvalidCashOut(address caller, uint256 contextProjectId, uint256 projectId, uint256 msgValue);
34
+ error JB721Hook_InvalidPay(address caller, uint256 contextProjectId, uint256 projectId);
35
35
  error JB721Hook_UnauthorizedToken(uint256 tokenId, address holder);
36
- error JB721Hook_UnexpectedTokenCashedOut();
36
+ error JB721Hook_UnexpectedTokenCashedOut(uint256 cashOutCount);
37
37
 
38
38
  //*********************************************************************//
39
39
  // --------------- public immutable stored properties ---------------- //
@@ -94,7 +94,9 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
94
94
  )
95
95
  {
96
96
  // Make sure (fungible) project tokens aren't also being cashed out.
97
- if (context.cashOutCount > 0) revert JB721Hook_UnexpectedTokenCashedOut();
97
+ if (context.cashOutCount > 0) {
98
+ revert JB721Hook_UnexpectedTokenCashedOut({cashOutCount: context.cashOutCount});
99
+ }
98
100
 
99
101
  // Fetch the cash out hook metadata using the corresponding metadata ID.
100
102
  (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
@@ -185,7 +187,6 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
185
187
  /// `context.beneficiary`. Part of `IJBCashOutHook`.
186
188
  /// @dev Reverts if the calling contract is not one of the project's terminals.
187
189
  /// @param context The cash out context passed in by the terminal.
188
- // slither-disable-next-line locked-ether
189
190
  function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
190
191
  external
191
192
  payable
@@ -200,7 +201,11 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
200
201
  if (
201
202
  msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
202
203
  || context.projectId != projectId
203
- ) revert JB721Hook_InvalidCashOut();
204
+ ) {
205
+ revert JB721Hook_InvalidCashOut({
206
+ caller: msg.sender, contextProjectId: context.projectId, projectId: projectId, msgValue: msg.value
207
+ });
208
+ }
204
209
 
205
210
  // Fetch the cash out hook metadata using the corresponding metadata ID.
206
211
  (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
@@ -237,7 +242,6 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
237
242
  /// `IJBPayHook`.
238
243
  /// @dev Reverts if the calling contract is not one of the project's terminals.
239
244
  /// @param context The payment context passed in by the terminal.
240
- // slither-disable-next-line locked-ether
241
245
  function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
242
246
  uint256 projectId = PROJECT_ID;
243
247
 
@@ -247,7 +251,7 @@ abstract contract JB721Hook is ERC721, IJB721Hook {
247
251
  !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
248
252
  || context.projectId != projectId
249
253
  ) {
250
- revert JB721Hook_InvalidPay();
254
+ revert JB721Hook_InvalidPay({caller: msg.sender, contextProjectId: context.projectId, projectId: projectId});
251
255
  }
252
256
 
253
257
  // Process the payment.
@@ -14,11 +14,11 @@ interface IJB721Checkpoints is IERC5805 {
14
14
  function HOOK() external view returns (address);
15
15
 
16
16
  /// @notice The owner of an NFT at a past block.
17
- /// @dev Mints do not write per-token checkpoint storage. Until a token's first non-mint transfer, ownership is
18
- /// inferred from the hook's `firstOwnerOf`.
17
+ /// @dev Returns `address(0)` for tokens that have never been enrolled (via `delegate(address, uint256[])`) or
18
+ /// transferred. Unenrolled tokens are ineligible for snapshot-based distribution.
19
19
  /// @param tokenId The token ID of the NFT to get the historical owner of.
20
20
  /// @param blockNumber The block number to look up.
21
- /// @return The owner of the token at `blockNumber`, or zero if the token has no known owner.
21
+ /// @return The owner of the token at `blockNumber`, or zero if the token is unenrolled or has no known owner.
22
22
  function ownerOfAt(uint256 tokenId, uint256 blockNumber) external view returns (address);
23
23
 
24
24
  /// @notice The store that holds tier and voting data for the hook's NFTs.
@@ -26,6 +26,14 @@ interface IJB721Checkpoints is IERC5805 {
26
26
  // forge-lint: disable-next-line(mixed-case-function)
27
27
  function STORE() external view returns (IJB721TiersHookStore);
28
28
 
29
+ /// @notice Delegates voting power and enrolls tokens for distribution eligibility.
30
+ /// @dev Writes per-token owner checkpoints so `ownerOfAt` can prove ownership at past blocks.
31
+ /// Only the current token owner can enroll. Tokens without checkpoints are ineligible for snapshot-based
32
+ /// distribution.
33
+ /// @param delegatee The address to delegate voting power to. Use your own address for self-delegation.
34
+ /// @param tokenIds The token IDs to enroll for distribution eligibility.
35
+ function delegate(address delegatee, uint256[] calldata tokenIds) external;
36
+
29
37
  /// @notice Initializes a cloned module with its hook reference.
30
38
  /// @dev Can only be called once. Called by the deployer after cloning.
31
39
  /// @param hook The hook this module serves.
@@ -33,7 +41,6 @@ interface IJB721Checkpoints is IERC5805 {
33
41
 
34
42
  /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
35
43
  /// @dev Looks up the token's tier voting units from the store internally.
36
- /// Auto-self-delegates on first receive so checkpoints work without manual delegation.
37
44
  /// @param from The previous owner (address(0) on mint).
38
45
  /// @param to The new owner (address(0) on burn).
39
46
  /// @param tokenId The token ID to transfer (used to look up tier voting units).
@@ -6,9 +6,6 @@ import {IJB721TiersHookStore} from "./IJB721TiersHookStore.sol";
6
6
 
7
7
  /// @notice Deploys JB721Checkpoints clones for JB721TiersHook instances.
8
8
  interface IJB721CheckpointsDeployer {
9
- /// @notice Thrown when the caller is not the hook that the checkpoint module is deployed for.
10
- error JB721CheckpointsDeployer_Unauthorized();
11
-
12
9
  /// @notice The implementation contract that clones are based on.
13
10
  /// @return The implementation address.
14
11
  // forge-lint: disable-next-line(mixed-case-function)
@@ -18,14 +18,6 @@ import {JB721TiersSetDiscountPercentConfig} from "../structs/JB721TiersSetDiscou
18
18
 
19
19
  /// @notice A 721 tiers hook that mints tiered NFTs for payments and tracks their cash out weight.
20
20
  interface IJB721TiersHook is IJB721Hook {
21
- /// @notice Emitted when an `addToBalanceOf` call reverts during leftover distribution. The funds remain
22
- /// stranded in the hook contract.
23
- /// @param projectId The project ID whose terminal reverted.
24
- /// @param token The token to send.
25
- /// @param amount The amount that failed to send.
26
- /// @param reason The revert reason bytes.
27
- event AddToBalanceReverted(uint256 indexed projectId, address token, uint256 amount, bytes reason);
28
-
29
21
  /// @notice Emitted when pay credits are added for an account.
30
22
  /// @param amount The amount of credits added.
31
23
  /// @param newTotalCredits The new total credits balance for the account.
@@ -3,6 +3,7 @@ pragma solidity 0.8.28;
3
3
 
4
4
  /// @notice Global constants used across 721 hook contracts.
5
5
  library JB721Constants {
6
+ /// @notice The denominator used when applying tier discount percentages.
6
7
  uint16 public constant DISCOUNT_DENOMINATOR = 200;
7
8
 
8
9
  /// @notice The metadata ID used to identify the 721 beneficiary entry in payment metadata.
@@ -31,7 +31,7 @@ library JB721TiersHookLib {
31
31
  // --------------------------- custom errors ------------------------- //
32
32
  //*********************************************************************//
33
33
 
34
- error JB721TiersHook_CantBuyWithCredits();
34
+ error JB721TiersHook_CantBuyWithCredits(uint256 restrictedCost, uint256 freshValue);
35
35
  error JB721TiersHook_Overspending(uint256 leftoverAmount);
36
36
  error JB721TiersHookLib_NoTerminalForLeftover(uint256 projectId, address token, uint256 leftoverAmount);
37
37
  error JB721TiersHookLib_SplitFallbackFailed(uint256 projectId, address token, uint256 amount, bytes reason);
@@ -79,7 +79,6 @@ library JB721TiersHookLib {
79
79
  ++i;
80
80
  }
81
81
  }
82
- // slither-disable-next-line reentrancy-events
83
82
  store.recordRemoveTierIds(tierIdsToRemove);
84
83
  }
85
84
 
@@ -87,7 +86,6 @@ library JB721TiersHookLib {
87
86
  if (tiersToAdd.length != 0) {
88
87
  uint256[] memory tierIdsAdded = store.recordAddTiers(tiersToAdd);
89
88
 
90
- // slither-disable-next-line reentrancy-events
91
89
  for (uint256 i; i < tiersToAdd.length;) {
92
90
  emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: caller});
93
91
 
@@ -135,7 +133,9 @@ library JB721TiersHookLib {
135
133
  SafeERC20.safeTransferFrom({token: IERC20(token), from: msg.sender, to: address(this), value: amount});
136
134
  uint256 receivedAmount = IERC20(token).balanceOf(address(this)) - balanceBefore;
137
135
  if (receivedAmount != amount) {
138
- revert JB721TiersHookLib_TokenTransferAmountMismatch(amount, receivedAmount);
136
+ revert JB721TiersHookLib_TokenTransferAmountMismatch({
137
+ expectedAmount: amount, receivedAmount: receivedAmount
138
+ });
139
139
  }
140
140
  }
141
141
 
@@ -223,16 +223,19 @@ library JB721TiersHookLib {
223
223
  if (tierIdsToMint.length != 0) {
224
224
  uint256 restrictedCost;
225
225
 
226
- // slither-disable-next-line reentrancy-events,reentrancy-no-eth
227
226
  (tokenIds, leftoverAmount, restrictedCost) =
228
227
  store.recordMint({amount: leftoverAmount, tierIds: tierIdsToMint, isOwnerMint: false});
229
228
 
230
229
  // Credit-restricted tiers must be fully covered by fresh payment (not stored credits).
231
- if (restrictedCost > value) revert JB721TiersHook_CantBuyWithCredits();
230
+ if (restrictedCost > value) {
231
+ revert JB721TiersHook_CantBuyWithCredits({restrictedCost: restrictedCost, freshValue: value});
232
+ }
232
233
  }
233
234
 
234
235
  // If overspending isn't allowed, revert.
235
- if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
236
+ if (leftoverAmount != 0 && !allowOverspending) {
237
+ revert JB721TiersHook_Overspending({leftoverAmount: leftoverAmount});
238
+ }
236
239
 
237
240
  // Compute the new pay credits balance: leftover + unused credits (held in newPayCredits).
238
241
  newPayCredits = leftoverAmount + newPayCredits;
@@ -259,7 +262,6 @@ library JB721TiersHookLib {
259
262
  uint256[] memory tierIdsAdded = store.recordAddTiers(tiersToAdd);
260
263
 
261
264
  for (uint256 i; i < tiersToAdd.length;) {
262
- // slither-disable-next-line reentrancy-events
263
265
  emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: caller});
264
266
 
265
267
  unchecked {
@@ -489,7 +491,6 @@ library JB721TiersHookLib {
489
491
 
490
492
  for (uint256 i; i < tierIdsToMint.length;) {
491
493
  // Get only the pricing fields (lightweight — avoids full struct construction).
492
- // slither-disable-next-line calls-loop
493
494
  (uint104 tierPrice, uint32 tierSplitPercent, uint8 tierDiscountPercent) =
494
495
  store.tierPricingOf({hook: hook, id: tierIdsToMint[i]});
495
496
  if (tierSplitPercent != 0) {
@@ -707,7 +708,6 @@ library JB721TiersHookLib {
707
708
  )
708
709
  private
709
710
  {
710
- // slither-disable-next-line calls-loop
711
711
  JBSplit[] memory tierSplits = splitsContract.splitsOf({projectId: projectId, rulesetId: 0, groupId: groupId});
712
712
 
713
713
  bool isNativeToken = token == JBConstants.NATIVE_TOKEN;
@@ -725,7 +725,6 @@ library JB721TiersHookLib {
725
725
  // On failure, don't re-add to leftoverAmount — this prevents inflating later recipients.
726
726
  // Failed amounts accumulate as the gap between `amount` and `leftoverAmount + total sent`.
727
727
  // After the loop, we re-add leftoverPercentage-based residual naturally.
728
- // slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-benign,reentrancy-events
729
728
  if (!_sendPayoutToSplit({
730
729
  directory: directory,
731
730
  split: tierSplits[j],
@@ -752,14 +751,14 @@ library JB721TiersHookLib {
752
751
  leftoverAmount += amount;
753
752
 
754
753
  if (leftoverAmount != 0) {
755
- // slither-disable-next-line calls-loop
756
754
  IJBTerminal terminal = directory.primaryTerminalOf({projectId: projectId, token: token});
757
755
  // Revert if there are leftover funds but no terminal to route them to.
758
756
  if (address(terminal) == address(0)) {
759
- revert JB721TiersHookLib_NoTerminalForLeftover(projectId, token, leftoverAmount);
757
+ revert JB721TiersHookLib_NoTerminalForLeftover({
758
+ projectId: projectId, token: token, leftoverAmount: leftoverAmount
759
+ });
760
760
  }
761
761
  if (isNativeToken) {
762
- // slither-disable-next-line arbitrary-send-eth,calls-loop
763
762
  try terminal.addToBalanceOf{value: leftoverAmount}({
764
763
  projectId: projectId,
765
764
  token: token,
@@ -769,11 +768,12 @@ library JB721TiersHookLib {
769
768
  metadata: bytes("")
770
769
  }) {}
771
770
  catch (bytes memory reason) {
772
- revert JB721TiersHookLib_SplitFallbackFailed(projectId, token, leftoverAmount, reason);
771
+ revert JB721TiersHookLib_SplitFallbackFailed({
772
+ projectId: projectId, token: token, amount: leftoverAmount, reason: reason
773
+ });
773
774
  }
774
775
  } else {
775
776
  SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: leftoverAmount});
776
- // slither-disable-next-line calls-loop
777
777
  try terminal.addToBalanceOf({
778
778
  projectId: projectId,
779
779
  token: token,
@@ -785,7 +785,9 @@ library JB721TiersHookLib {
785
785
  catch (bytes memory reason) {
786
786
  // Reset approval on failure.
787
787
  SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
788
- revert JB721TiersHookLib_SplitFallbackFailed(projectId, token, leftoverAmount, reason);
788
+ revert JB721TiersHookLib_SplitFallbackFailed({
789
+ projectId: projectId, token: token, amount: leftoverAmount, reason: reason
790
+ });
789
791
  }
790
792
  }
791
793
  }
@@ -817,11 +819,9 @@ library JB721TiersHookLib {
817
819
  if (isNativeToken) {
818
820
  // Wrap in try-catch so a reverting hook doesn't brick all project payments.
819
821
  // On revert, ETH stays with the caller and we return false.
820
- // slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-events
821
822
  try split.hook.processSplitWith{value: amount}(context) {
822
823
  return true;
823
824
  } catch (bytes memory reason) {
824
- // slither-disable-next-line reentrancy-events
825
825
  emit SplitPayoutReverted({
826
826
  projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
827
827
  });
@@ -834,10 +834,8 @@ library JB721TiersHookLib {
834
834
  // cause the caller to skip subtracting this amount from leftoverAmount, leading
835
835
  // to a double-spend when the leftover is later sent to the project's balance.
836
836
  SafeERC20.safeTransfer({token: IERC20(token), to: address(split.hook), value: amount});
837
- // slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-events
838
837
  try split.hook.processSplitWith(context) {}
839
838
  catch (bytes memory reason) {
840
- // slither-disable-next-line reentrancy-events
841
839
  emit SplitPayoutReverted({
842
840
  projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
843
841
  });
@@ -845,14 +843,12 @@ library JB721TiersHookLib {
845
843
  return true;
846
844
  }
847
845
  } else if (split.projectId != 0) {
848
- // slither-disable-next-line calls-loop
849
846
  IJBTerminal terminal = directory.primaryTerminalOf({projectId: split.projectId, token: token});
850
847
  if (address(terminal) == address(0)) return false;
851
848
 
852
849
  // Wrap terminal calls in try-catch to prevent a failing terminal from bricking payments.
853
850
  if (split.preferAddToBalance) {
854
851
  if (isNativeToken) {
855
- // slither-disable-next-line arbitrary-send-eth,calls-loop,reentrancy-no-eth,reentrancy-events
856
852
  try terminal.addToBalanceOf{value: amount}({
857
853
  projectId: split.projectId,
858
854
  token: token,
@@ -863,7 +859,6 @@ library JB721TiersHookLib {
863
859
  }) {
864
860
  return true;
865
861
  } catch (bytes memory reason) {
866
- // slither-disable-next-line reentrancy-events
867
862
  emit SplitPayoutReverted({
868
863
  projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
869
864
  });
@@ -871,7 +866,6 @@ library JB721TiersHookLib {
871
866
  }
872
867
  } else {
873
868
  SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
874
- // slither-disable-next-line calls-loop,reentrancy-no-eth,reentrancy-events
875
869
  try terminal.addToBalanceOf({
876
870
  projectId: split.projectId,
877
871
  token: token,
@@ -884,7 +878,6 @@ library JB721TiersHookLib {
884
878
  } catch (bytes memory reason) {
885
879
  // Reset approval on failure so tokens aren't left approved to the terminal.
886
880
  SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
887
- // slither-disable-next-line reentrancy-events
888
881
  emit SplitPayoutReverted({
889
882
  projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
890
883
  });
@@ -893,7 +886,6 @@ library JB721TiersHookLib {
893
886
  }
894
887
  } else {
895
888
  if (isNativeToken) {
896
- // slither-disable-next-line arbitrary-send-eth,unused-return,calls-loop,reentrancy-events
897
889
  try terminal.pay{value: amount}({
898
890
  projectId: split.projectId,
899
891
  token: token,
@@ -905,7 +897,6 @@ library JB721TiersHookLib {
905
897
  }) {
906
898
  return true;
907
899
  } catch (bytes memory reason) {
908
- // slither-disable-next-line reentrancy-events
909
900
  emit SplitPayoutReverted({
910
901
  projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
911
902
  });
@@ -913,7 +904,6 @@ library JB721TiersHookLib {
913
904
  }
914
905
  } else {
915
906
  SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: amount});
916
- // slither-disable-next-line unused-return,calls-loop,reentrancy-no-eth,reentrancy-events
917
907
  try terminal.pay({
918
908
  projectId: split.projectId,
919
909
  token: token,
@@ -927,7 +917,6 @@ library JB721TiersHookLib {
927
917
  } catch (bytes memory reason) {
928
918
  // Reset approval on failure so tokens aren't left approved to the terminal.
929
919
  SafeERC20.forceApprove({token: IERC20(token), spender: address(terminal), value: 0});
930
- // slither-disable-next-line reentrancy-events
931
920
  emit SplitPayoutReverted({
932
921
  projectId: projectId, split: split, amount: amount, reason: reason, caller: msg.sender
933
922
  });
@@ -937,7 +926,6 @@ library JB721TiersHookLib {
937
926
  }
938
927
  } else if (split.beneficiary != address(0)) {
939
928
  if (isNativeToken) {
940
- // slither-disable-next-line arbitrary-send-eth,calls-loop
941
929
  (bool success,) = split.beneficiary.call{value: amount}("");
942
930
  if (!success) return false;
943
931
  } else {
@@ -945,7 +933,6 @@ library JB721TiersHookLib {
945
933
  // false on failure instead of reverting. This handles non-standard tokens (e.g. USDT)
946
934
  // that return void, while routing failed transfers to the project's balance instead
947
935
  // of bricking all payments.
948
- // slither-disable-next-line calls-loop
949
936
  (bool callSuccess, bytes memory returndata) =
950
937
  address(token).call(abi.encodeCall(IERC20.transfer, (split.beneficiary, amount)));
951
938
  if (!callSuccess || (returndata.length != 0 && !abi.decode(returndata, (bool)))) return false;
@@ -32,7 +32,7 @@ library JBBitmap {
32
32
  /// @dev The `index` is the index that the bit would have if the bitmap were reshaped to a 1*n matrix.
33
33
  function isTierIdRemoved(mapping(uint256 => uint256) storage self, uint256 index) internal view returns (bool) {
34
34
  uint256 depth = _retrieveDepth(index);
35
- return isTierIdRemoved(JBBitmapWord({currentWord: self[depth], currentDepth: depth}), index);
35
+ return isTierIdRemoved({self: JBBitmapWord({currentWord: self[depth], currentDepth: depth}), index: index});
36
36
  }
37
37
 
38
38
  /// @notice Set the bit at the given index to true, indicating that the corresponding tier has been removed.
@@ -22,8 +22,8 @@ pragma solidity ^0.8.0;
22
22
  /// @custom:member allowAddPriceFeed A flag indicating if a project can add new price feeds to calculate exchange rates
23
23
  /// between its tokens.
24
24
  /// @custom:member holdFees A flag indicating if fees should be held during this ruleset.
25
- /// @custom:member useTotalSurplusForCashOut A flag indicating if cash outs should use the project's balance held
26
- /// in all terminals instead of the project's local terminal balance from which the cash out is fulfilled.
25
+ /// @custom:member scopeCashOutsToLocalBalances A flag indicating if omnichain cash-out calculations should use only
26
+ /// the local chain's terminal balance instead of the project's balance held in all terminals.
27
27
  /// @custom:member useDataHookForCashOuts A flag indicating if the data hook should be used for cash out transactions
28
28
  /// during
29
29
  /// this ruleset.
@@ -43,7 +43,7 @@ struct JBPayDataHookRulesetMetadata {
43
43
  bool allowAddPriceFeed;
44
44
  bool ownerMustSendPayouts;
45
45
  bool holdFees;
46
- bool useTotalSurplusForCashOuts;
46
+ bool scopeCashOutsToLocalBalances;
47
47
  bool useDataHookForCashOut;
48
48
  uint16 metadata;
49
49
  }
@@ -223,7 +223,7 @@ contract UnitTestSetup is Test {
223
223
  allowAddPriceFeed: false,
224
224
  ownerMustSendPayouts: false,
225
225
  holdFees: false,
226
- useTotalSurplusForCashOuts: false,
226
+ scopeCashOutsToLocalBalances: true,
227
227
  useDataHookForPay: true,
228
228
  useDataHookForCashOut: true,
229
229
  dataHook: address(0),
@@ -772,7 +772,7 @@ contract UnitTestSetup is Test {
772
772
  allowAddAccountingContext: false,
773
773
  allowAddPriceFeed: false,
774
774
  holdFees: false,
775
- useTotalSurplusForCashOuts: false,
775
+ scopeCashOutsToLocalBalances: true,
776
776
  useDataHookForCashOut: false,
777
777
  metadata: 0x00
778
778
  });