@bananapus/721-hook-v6 0.0.51 → 0.0.53

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/721-hook-v6",
3
- "version": "0.0.51",
3
+ "version": "0.0.53",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -24,20 +24,14 @@ contract DeployScript is Script, Sphinx {
24
24
  AddressRegistryDeployment registry;
25
25
 
26
26
  /// @notice The address that is allowed to forward calls to the terminal and controller on a users behalf.
27
- // forge-lint: disable-next-line(mixed-case-variable)
28
- address private TRUSTED_FORWARDER;
27
+ address private trustedForwarder;
29
28
 
30
29
  /// @notice the salts that are used to deploy the contracts.
31
- // forge-lint: disable-next-line(mixed-case-variable)
32
- bytes32 HOOK_SALT = "JB721TiersHookV6_";
33
- // forge-lint: disable-next-line(mixed-case-variable)
34
- bytes32 HOOK_DEPLOYER_SALT = "JB721TiersHookDeployerV6_";
35
- // forge-lint: disable-next-line(mixed-case-variable)
36
- bytes32 HOOK_STORE_SALT = "JB721TiersHookStoreV6_";
37
- // forge-lint: disable-next-line(mixed-case-variable)
38
- bytes32 PROJECT_DEPLOYER_SALT = "JB721TiersHookProjectDeployerV6";
39
- // forge-lint: disable-next-line(mixed-case-variable)
40
- bytes32 CHECKPOINTS_DEPLOYER_SALT = "JB721CheckpointsDeployerV6";
30
+ bytes32 private constant _HOOK_SALT = "JB721TiersHookV6_";
31
+ bytes32 private constant _HOOK_DEPLOYER_SALT = "JB721TiersHookDeployerV6_";
32
+ bytes32 private constant _HOOK_STORE_SALT = "JB721TiersHookStoreV6_";
33
+ bytes32 private constant _PROJECT_DEPLOYER_SALT = "JB721TiersHookProjectDeployerV6";
34
+ bytes32 private constant _CHECKPOINTS_DEPLOYER_SALT = "JB721CheckpointsDeployerV6";
41
35
 
42
36
  function configureSphinx() public override {
43
37
  sphinxConfig.projectName = "nana-721-hook-v6";
@@ -53,7 +47,7 @@ contract DeployScript is Script, Sphinx {
53
47
  );
54
48
 
55
49
  // We use the same trusted forwarder as the core deployment.
56
- TRUSTED_FORWARDER = core.permissions.trustedForwarder();
50
+ trustedForwarder = core.permissions.trustedForwarder();
57
51
 
58
52
  registry = AddressRegistryDeploymentLib.getDeployment(
59
53
  vm.envOr(
@@ -74,25 +68,25 @@ contract DeployScript is Script, Sphinx {
74
68
  {
75
69
  // Perform the check for the store.
76
70
  (address _store, bool _storeIsDeployed) = _isDeployed({
77
- salt: HOOK_STORE_SALT, creationCode: type(JB721TiersHookStore).creationCode, arguments: ""
71
+ salt: _HOOK_STORE_SALT, creationCode: type(JB721TiersHookStore).creationCode, arguments: ""
78
72
  });
79
73
 
80
74
  // Deploy it if it has not been deployed yet.
81
- store = !_storeIsDeployed ? new JB721TiersHookStore{salt: HOOK_STORE_SALT}() : JB721TiersHookStore(_store);
75
+ store = !_storeIsDeployed ? new JB721TiersHookStore{salt: _HOOK_STORE_SALT}() : JB721TiersHookStore(_store);
82
76
  }
83
77
 
84
78
  JB721CheckpointsDeployer checkpointsDeployer;
85
79
  {
86
80
  // Perform the check for the deployer.
87
81
  (address _deployer, bool _deployerIsDeployed) = _isDeployed({
88
- salt: CHECKPOINTS_DEPLOYER_SALT,
82
+ salt: _CHECKPOINTS_DEPLOYER_SALT,
89
83
  creationCode: type(JB721CheckpointsDeployer).creationCode,
90
84
  arguments: abi.encode(store)
91
85
  });
92
86
 
93
87
  // Deploy it if it has not been deployed yet.
94
88
  checkpointsDeployer = !_deployerIsDeployed
95
- ? new JB721CheckpointsDeployer{salt: CHECKPOINTS_DEPLOYER_SALT}(store)
89
+ ? new JB721CheckpointsDeployer{salt: _CHECKPOINTS_DEPLOYER_SALT}(store)
96
90
  : JB721CheckpointsDeployer(_deployer);
97
91
  }
98
92
 
@@ -100,7 +94,7 @@ contract DeployScript is Script, Sphinx {
100
94
  {
101
95
  // Perform the check for the registry.
102
96
  (address _hook, bool _hookIsDeployed) = _isDeployed({
103
- salt: HOOK_SALT,
97
+ salt: _HOOK_SALT,
104
98
  creationCode: type(JB721TiersHook).creationCode,
105
99
  arguments: abi.encode(
106
100
  core.directory,
@@ -110,22 +104,22 @@ contract DeployScript is Script, Sphinx {
110
104
  store,
111
105
  core.splits,
112
106
  checkpointsDeployer,
113
- TRUSTED_FORWARDER
107
+ trustedForwarder
114
108
  )
115
109
  });
116
110
 
117
111
  // Deploy it if it has not been deployed yet.
118
112
  hook = !_hookIsDeployed
119
- ? new JB721TiersHook{salt: HOOK_SALT}(
120
- core.directory,
121
- core.permissions,
122
- core.prices,
123
- core.rulesets,
124
- store,
125
- core.splits,
126
- IJB721CheckpointsDeployer(address(checkpointsDeployer)),
127
- TRUSTED_FORWARDER
128
- )
113
+ ? new JB721TiersHook{salt: _HOOK_SALT}({
114
+ directory: core.directory,
115
+ permissions: core.permissions,
116
+ prices: core.prices,
117
+ rulesets: core.rulesets,
118
+ store: store,
119
+ splits: core.splits,
120
+ checkpointsDeployer: IJB721CheckpointsDeployer(address(checkpointsDeployer)),
121
+ trustedForwarder: trustedForwarder
122
+ })
129
123
  : JB721TiersHook(_hook);
130
124
  }
131
125
 
@@ -133,15 +127,15 @@ contract DeployScript is Script, Sphinx {
133
127
  {
134
128
  // Perform the check for the registry.
135
129
  (address _hookDeployer, bool _hookDeployerIsDeployed) = _isDeployed({
136
- salt: HOOK_DEPLOYER_SALT,
130
+ salt: _HOOK_DEPLOYER_SALT,
137
131
  creationCode: type(JB721TiersHookDeployer).creationCode,
138
- arguments: abi.encode(hook, store, registry.registry, TRUSTED_FORWARDER)
132
+ arguments: abi.encode(hook, store, registry.registry, trustedForwarder)
139
133
  });
140
134
 
141
135
  hookDeployer = !_hookDeployerIsDeployed
142
- ? new JB721TiersHookDeployer{salt: HOOK_DEPLOYER_SALT}(
143
- hook, store, registry.registry, TRUSTED_FORWARDER
144
- )
136
+ ? new JB721TiersHookDeployer{salt: _HOOK_DEPLOYER_SALT}({
137
+ hook: hook, store: store, addressRegistry: registry.registry, trustedForwarder: trustedForwarder
138
+ })
145
139
  : JB721TiersHookDeployer(_hookDeployer);
146
140
  }
147
141
 
@@ -149,15 +143,18 @@ contract DeployScript is Script, Sphinx {
149
143
  {
150
144
  // Perform the check for the registry.
151
145
  (address _projectDeployer, bool _projectDeployerIsdeployed) = _isDeployed({
152
- salt: PROJECT_DEPLOYER_SALT,
146
+ salt: _PROJECT_DEPLOYER_SALT,
153
147
  creationCode: type(JB721TiersHookProjectDeployer).creationCode,
154
- arguments: abi.encode(core.directory, core.permissions, hookDeployer, TRUSTED_FORWARDER)
148
+ arguments: abi.encode(core.directory, core.permissions, hookDeployer, trustedForwarder)
155
149
  });
156
150
 
157
151
  projectDeployer = !_projectDeployerIsdeployed
158
- ? new JB721TiersHookProjectDeployer{salt: PROJECT_DEPLOYER_SALT}(
159
- core.directory, core.permissions, hookDeployer, TRUSTED_FORWARDER
160
- )
152
+ ? new JB721TiersHookProjectDeployer{salt: _PROJECT_DEPLOYER_SALT}({
153
+ directory: core.directory,
154
+ permissions: core.permissions,
155
+ hookDeployer: hookDeployer,
156
+ trustedForwarder: trustedForwarder
157
+ })
161
158
  : JB721TiersHookProjectDeployer(_projectDeployer);
162
159
  }
163
160
  }
@@ -11,10 +11,8 @@ import {IJB721TiersHookProjectDeployer} from "../../src/interfaces/IJB721TiersHo
11
11
  import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
12
12
 
13
13
  struct Hook721Deployment {
14
- // forge-lint: disable-next-line(mixed-case-variable)
15
- IJB721TiersHookDeployer hook_deployer;
16
- // forge-lint: disable-next-line(mixed-case-variable)
17
- IJB721TiersHookProjectDeployer project_deployer;
14
+ IJB721TiersHookDeployer hookDeployer;
15
+ IJB721TiersHookProjectDeployer projectDeployer;
18
16
  IJB721TiersHookStore store;
19
17
  }
20
18
 
@@ -25,17 +23,16 @@ library Hook721DeploymentLib {
25
23
  Vm internal constant vm = Vm(VM_ADDRESS);
26
24
 
27
25
  function getDeployment(string memory path) internal returns (Hook721Deployment memory deployment) {
28
- // get chainId for which we need to get the deployment.
26
+ // Match the current chain ID to the Sphinx network name used in deployment artifacts.
29
27
  uint256 chainId = block.chainid;
30
28
 
31
- // Deploy to get the constants.
32
- // TODO: get constants without deploy.
29
+ // `SphinxConstants` exposes Sphinx's supported chain ID to network name mapping.
33
30
  SphinxConstants sphinxConstants = new SphinxConstants();
34
31
  NetworkInfo[] memory networks = sphinxConstants.getNetworkInfoArray();
35
32
 
36
33
  for (uint256 _i; _i < networks.length; _i++) {
37
34
  if (networks[_i].chainId == chainId) {
38
- return getDeployment({path: path, network_name: networks[_i].name});
35
+ return getDeployment({path: path, networkName: networks[_i].name});
39
36
  }
40
37
  }
41
38
 
@@ -44,27 +41,26 @@ library Hook721DeploymentLib {
44
41
 
45
42
  function getDeployment(
46
43
  string memory path,
47
- // forge-lint: disable-next-line(mixed-case-variable)
48
- string memory network_name
44
+ string memory networkName
49
45
  )
50
46
  internal
51
47
  view
52
48
  returns (Hook721Deployment memory deployment)
53
49
  {
54
- deployment.hook_deployer = IJB721TiersHookDeployer(
50
+ deployment.hookDeployer = IJB721TiersHookDeployer(
55
51
  _getDeploymentAddress({
56
52
  path: path,
57
- project_name: "nana-721-hook-v6",
58
- network_name: network_name,
53
+ projectName: "nana-721-hook-v6",
54
+ networkName: networkName,
59
55
  contractName: "JB721TiersHookDeployer"
60
56
  })
61
57
  );
62
58
 
63
- deployment.project_deployer = IJB721TiersHookProjectDeployer(
59
+ deployment.projectDeployer = IJB721TiersHookProjectDeployer(
64
60
  _getDeploymentAddress({
65
61
  path: path,
66
- project_name: "nana-721-hook-v6",
67
- network_name: network_name,
62
+ projectName: "nana-721-hook-v6",
63
+ networkName: networkName,
68
64
  contractName: "JB721TiersHookProjectDeployer"
69
65
  })
70
66
  );
@@ -72,8 +68,8 @@ library Hook721DeploymentLib {
72
68
  deployment.store = IJB721TiersHookStore(
73
69
  _getDeploymentAddress({
74
70
  path: path,
75
- project_name: "nana-721-hook-v6",
76
- network_name: network_name,
71
+ projectName: "nana-721-hook-v6",
72
+ networkName: networkName,
77
73
  contractName: "JB721TiersHookStore"
78
74
  })
79
75
  );
@@ -86,10 +82,8 @@ library Hook721DeploymentLib {
86
82
  /// @return The address of the contract.
87
83
  function _getDeploymentAddress(
88
84
  string memory path,
89
- // forge-lint: disable-next-line(mixed-case-variable)
90
- string memory project_name,
91
- // forge-lint: disable-next-line(mixed-case-variable)
92
- string memory network_name,
85
+ string memory projectName,
86
+ string memory networkName,
93
87
  string memory contractName
94
88
  )
95
89
  internal
@@ -98,7 +92,7 @@ library Hook721DeploymentLib {
98
92
  {
99
93
  string memory deploymentJson =
100
94
  // forge-lint: disable-next-line(unsafe-cheatcode)
101
- vm.readFile(string.concat(path, project_name, "/", network_name, "/", contractName, ".json"));
102
- return stdJson.readAddress(deploymentJson, ".address");
95
+ vm.readFile(string.concat(path, projectName, "/", networkName, "/", contractName, ".json"));
96
+ return stdJson.readAddress({json: deploymentJson, key: ".address"});
103
97
  }
104
98
  }
@@ -40,7 +40,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
40
40
  //*********************************************************************//
41
41
 
42
42
  /// @notice The hook that this module tracks voting power for.
43
- address public override HOOK;
43
+ address public override hook;
44
44
 
45
45
  //*********************************************************************//
46
46
  // -------------------- internal stored properties ------------------- //
@@ -59,7 +59,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
59
59
  /// @param store The store that holds tier data for each hook's NFTs.
60
60
  constructor(IJB721TiersHookStore store) EIP712("JB721Checkpoints", "1") {
61
61
  STORE = store;
62
- HOOK = address(1);
62
+ hook = address(1);
63
63
  }
64
64
 
65
65
  //*********************************************************************//
@@ -81,7 +81,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
81
81
  uint256 tokenId = tokenIds[i];
82
82
 
83
83
  // Only the current owner can enroll their tokens.
84
- if (IERC721(HOOK).ownerOf(tokenId) != msg.sender) {
84
+ if (IERC721(hook).ownerOf(tokenId) != msg.sender) {
85
85
  revert JB721Checkpoints_NotOwner({tokenId: tokenId, caller: msg.sender});
86
86
  }
87
87
 
@@ -99,20 +99,20 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
99
99
 
100
100
  /// @notice Initializes a cloned module with its hook reference.
101
101
  /// @dev Can only be called once. Called by the deployer after cloning.
102
- /// @param hook The hook this module serves.
103
- function initialize(address hook) external override {
104
- if (HOOK != address(0)) revert JB721Checkpoints_AlreadyInitialized({hook: HOOK});
102
+ /// @param hookAddress The hook this module serves.
103
+ function initialize(address hookAddress) external override {
104
+ if (hook != address(0)) revert JB721Checkpoints_AlreadyInitialized({hook: hook});
105
105
  // `hook` cannot be zero when called through the deployer because `msg.sender` must equal `hook`.
106
- HOOK = hook;
106
+ hook = hookAddress;
107
107
  }
108
108
 
109
109
  /// @notice Called by the hook after every NFT transfer to update checkpointed voting power.
110
- /// @dev Only callable by the HOOK. Looks up the token's tier voting units from the store.
110
+ /// @dev Only callable by the hook. Looks up the token's tier voting units from the store.
111
111
  /// @param from The previous owner (address(0) on mint).
112
112
  /// @param to The new owner (address(0) on burn).
113
113
  /// @param tokenId The token ID to transfer.
114
114
  function onTransfer(address from, address to, uint256 tokenId) external override {
115
- if (msg.sender != HOOK) revert JB721Checkpoints_Unauthorized({caller: msg.sender, hook: HOOK});
115
+ if (msg.sender != hook) revert JB721Checkpoints_Unauthorized({caller: msg.sender, hook: hook});
116
116
 
117
117
  if (from != address(0)) {
118
118
  // forge-lint: disable-next-line(unsafe-typecast)
@@ -120,7 +120,7 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
120
120
  }
121
121
 
122
122
  // Look up this token's tier to get its voting units.
123
- uint256 votingUnits = STORE.tierOfTokenId({hook: HOOK, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
123
+ uint256 votingUnits = STORE.tierOfTokenId({hook: hook, tokenId: tokenId, includeResolvedUri: false}).votingUnits;
124
124
 
125
125
  // Move checkpointed voting power from the previous owner to the new owner.
126
126
  _transferVotingUnits({from: from, to: to, amount: votingUnits});
@@ -161,6 +161,6 @@ contract JB721Checkpoints is Votes, IJB721Checkpoints {
161
161
  /// @param account The address to get the voting units of.
162
162
  /// @return The total voting units the account holds.
163
163
  function _getVotingUnits(address account) internal view override returns (uint256) {
164
- return STORE.votingUnitsOf({hook: HOOK, account: account});
164
+ return STORE.votingUnitsOf({hook: hook, account: account});
165
165
  }
166
166
  }
@@ -107,7 +107,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
107
107
 
108
108
  /// @notice The checkpoint module that manages IVotes-compatible checkpointed voting power for this hook's NFTs.
109
109
  /// @dev Lazily deployed on the first transfer. Pass this to JBTokenDistributor as the IVotes token.
110
- IJB721Checkpoints public override CHECKPOINTS;
110
+ IJB721Checkpoints public override checkpoints;
111
111
 
112
112
  //*********************************************************************//
113
113
  // -------------------------- constructor ---------------------------- //
@@ -232,9 +232,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
232
232
  /// @notice Initialize a cloned copy of the hook. Sets the project association, ERC-721 name/symbol, pricing
233
233
  /// context (currency + decimals), metadata URIs, initial tiers, and behavioral flags. Can only be called once
234
234
  /// per clone — the implementation contract is pre-initialized in its constructor to prevent misuse.
235
- /// @dev Called by `JB721TiersHookDeployer` immediately after cloning. Reverts with
236
- /// `JB721TiersHook_AlreadyInitialized` if called more than once, or `JB721TiersHook_NoProjectId` if projectId is 0.
237
- /// @param projectId The ID of the project this hook is associated with.
235
+ /// @dev Called by `JB721TiersHookDeployer` immediately after cloning. Reverts if called more than once or if the
236
+ /// project ID is zero.
237
+ /// @param initialProjectId The ID of the project this hook is associated with.
238
238
  /// @param name The name of the NFT collection.
239
239
  /// @param symbol The symbol representing the NFT collection.
240
240
  /// @param baseUri The URI to use as a base for full NFT `tokenUri`s.
@@ -244,7 +244,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
244
244
  /// category (from least to greatest).
245
245
  /// @param flags A set of additional options which dictate how the hook behaves.
246
246
  function initialize(
247
- uint256 projectId,
247
+ uint256 initialProjectId,
248
248
  string memory name,
249
249
  string memory symbol,
250
250
  string memory baseUri,
@@ -258,14 +258,14 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
258
258
  {
259
259
  // Stop re-initialization. This protects both the implementation contract (initialized in constructor)
260
260
  // and clones (initialized via this function).
261
- if (_initialized) revert JB721TiersHook_AlreadyInitialized(PROJECT_ID);
261
+ if (_initialized) revert JB721TiersHook_AlreadyInitialized({projectId: projectId});
262
262
  _initialized = true;
263
263
 
264
- // Make sure a projectId is provided.
265
- if (projectId == 0) revert JB721TiersHook_NoProjectId({projectId: projectId});
264
+ // Make sure a project ID is provided.
265
+ if (initialProjectId == 0) revert JB721TiersHook_NoProjectId({projectId: initialProjectId});
266
266
 
267
267
  // Initialize the superclass.
268
- JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
268
+ JB721Hook._initialize({initialProjectId: initialProjectId, name: name, symbol: symbol});
269
269
 
270
270
  // Validate pricing decimals are within a reasonable range.
271
271
  if (tiersConfig.decimals > 18) revert JB721TiersHook_InvalidPricingDecimals(tiersConfig.decimals);
@@ -326,7 +326,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
326
326
  /// @return The token URI from the `tokenUriResolver` if it is set. If it isn't set, the token URI for the NFT's
327
327
  /// tier.
328
328
  function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
329
- return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
329
+ return
330
+ JB721TiersHookLib.resolveTokenURI({store: STORE, hook: address(this), baseUri: baseURI, tokenId: tokenId});
330
331
  }
331
332
 
332
333
  /// @notice The total cash-out weight across all outstanding NFTs and pending reserves. This is the denominator
@@ -348,15 +349,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
348
349
  /// @param tierIdsToRemove The IDs of the tiers to remove.
349
350
  function adjustTiers(JB721TierConfig[] calldata tiersToAdd, uint256[] calldata tierIdsToRemove) external override {
350
351
  // Enforce permissions.
351
- _requirePermissionFrom({
352
- account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.ADJUST_721_TIERS
353
- });
352
+ _requirePermissionFrom({account: owner(), projectId: projectId, permissionId: JBPermissionIds.ADJUST_721_TIERS});
354
353
 
355
354
  // Delegate to the library (via DELEGATECALL) for tier removal, addition, event emission, and split setting.
356
355
  JB721TiersHookLib.adjustTiersFor({
357
356
  store: STORE,
358
357
  splits: SPLITS,
359
- projectId: PROJECT_ID,
358
+ projectId: projectId,
360
359
  hookAddress: address(this),
361
360
  caller: _msgSender(),
362
361
  tiersToAdd: tiersToAdd,
@@ -379,7 +378,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
379
378
  returns (uint256[] memory tokenIds)
380
379
  {
381
380
  // Enforce permissions.
382
- _requirePermissionFrom({account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.MINT_721});
381
+ _requirePermissionFrom({account: owner(), projectId: projectId, permissionId: JBPermissionIds.MINT_721});
383
382
 
384
383
  // Record the mint. The token IDs returned correspond to the tiers passed in.
385
384
  (tokenIds,,) = STORE.recordMint({
@@ -417,7 +416,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
417
416
  function setDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
418
417
  // Enforce permissions.
419
418
  _requirePermissionFrom({
420
- account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
419
+ account: owner(), projectId: projectId, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
421
420
  });
422
421
  _setDiscountPercentOf({tierId: tierId, discountPercent: discountPercent});
423
422
  }
@@ -428,7 +427,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
428
427
  function setDiscountPercentsOf(JB721TiersSetDiscountPercentConfig[] calldata configs) external override {
429
428
  // Enforce permissions.
430
429
  _requirePermissionFrom({
431
- account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
430
+ account: owner(), projectId: projectId, permissionId: JBPermissionIds.SET_721_DISCOUNT_PERCENT
432
431
  });
433
432
 
434
433
  for (uint256 i; i < configs.length;) {
@@ -453,24 +452,22 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
453
452
  /// @param tokenUriResolver The new URI resolver. Pass `IJB721TokenUriResolver(address(this))` as a sentinel value
454
453
  /// to leave unchanged. `address(this)` is used instead of `address(0)` because `address(0)` is a valid value that
455
454
  /// clears the resolver.
456
- /// @param encodedIPFSUriTierId The ID of the tier to set the encoded IPFS URI of.
457
- /// @param encodedIPFSUri The encoded IPFS URI to set.
455
+ /// @param encodedIpfsUriTierId The ID of the tier to set the encoded IPFS URI of.
456
+ /// @param encodedIpfsUri The encoded IPFS URI to set.
458
457
  function setMetadata(
459
458
  string calldata name,
460
459
  string calldata symbol,
461
460
  string calldata baseUri,
462
461
  string calldata contractUri,
463
462
  IJB721TokenUriResolver tokenUriResolver,
464
- uint256 encodedIPFSUriTierId,
465
- bytes32 encodedIPFSUri
463
+ uint256 encodedIpfsUriTierId,
464
+ bytes32 encodedIpfsUri
466
465
  )
467
466
  external
468
467
  override
469
468
  {
470
469
  // Enforce permissions.
471
- _requirePermissionFrom({
472
- account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.SET_721_METADATA
473
- });
470
+ _requirePermissionFrom({account: owner(), projectId: projectId, permissionId: JBPermissionIds.SET_721_METADATA});
474
471
 
475
472
  // Cache _msgSender() at function entry to avoid repeated calls.
476
473
  address caller = _msgSender();
@@ -501,11 +498,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
501
498
  // Store the new URI resolver.
502
499
  _recordSetTokenUriResolver(tokenUriResolver);
503
500
  }
504
- if (encodedIPFSUriTierId != 0 && encodedIPFSUri != bytes32(0)) {
505
- emit SetEncodedIPFSUri({tierId: encodedIPFSUriTierId, encodedUri: encodedIPFSUri, caller: caller});
501
+ if (encodedIpfsUriTierId != 0 && encodedIpfsUri != bytes32(0)) {
502
+ emit SetEncodedIpfsUri({tierId: encodedIpfsUriTierId, encodedUri: encodedIpfsUri, caller: caller});
506
503
 
507
504
  // Store the new encoded IPFS URI.
508
- STORE.recordSetEncodedIPFSUriOf({tierId: encodedIPFSUriTierId, encodedIPFSUri: encodedIPFSUri});
505
+ STORE.recordSetEncodedIpfsUriOf({tierId: encodedIpfsUriTierId, encodedIpfsUri: encodedIpfsUri});
509
506
  }
510
507
  }
511
508
 
@@ -519,12 +516,12 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
519
516
  /// @param count The number of reserved NFTs to mint.
520
517
  function mintPendingReservesFor(uint256 tierId, uint256 count) public override {
521
518
  // Get a reference to the project's current ruleset.
522
- JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID);
519
+ JBRuleset memory ruleset = _currentRulesetOf(projectId);
523
520
 
524
521
  // Pending reserve mints must not be paused.
525
522
  if (JB721TiersRulesetMetadataResolver.mintPendingReservesPaused((JBRulesetMetadataResolver.metadata(ruleset))))
526
523
  {
527
- revert JB721TiersHook_MintReserveNftsPaused({projectId: PROJECT_ID, tierId: tierId});
524
+ revert JB721TiersHook_MintReserveNftsPaused({projectId: projectId, tierId: tierId});
528
525
  }
529
526
 
530
527
  // Record the reserved mint for the tier.
@@ -694,7 +691,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
694
691
  (value, valid) = JB721TiersHookLib.normalizePaymentValue({
695
692
  packedPricingContext: _packedPricingContext,
696
693
  prices: PRICES,
697
- projectId: PROJECT_ID,
694
+ projectId: projectId,
698
695
  amountValue: context.amount.value,
699
696
  amountCurrency: context.amount.currency,
700
697
  amountDecimals: context.amount.decimals
@@ -722,7 +719,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
722
719
  JB721TiersHookLib.distributeAll({
723
720
  directory: DIRECTORY,
724
721
  splits: SPLITS,
725
- projectId: PROJECT_ID,
722
+ projectId: projectId,
726
723
  hookAddress: address(this),
727
724
  token: context.forwardedAmount.token,
728
725
  amount: context.forwardedAmount.value,
@@ -766,7 +763,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
766
763
  // If transfers are pausable, check if they're paused.
767
764
  if (transfersPausable) {
768
765
  // Get a reference to the project's current ruleset.
769
- JBRuleset memory ruleset = _currentRulesetOf(PROJECT_ID);
766
+ JBRuleset memory ruleset = _currentRulesetOf(projectId);
770
767
 
771
768
  // If transfers are paused and the NFT isn't being transferred to the zero address, revert.
772
769
  if (
@@ -776,7 +773,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
776
773
  )
777
774
  ) {
778
775
  revert JB721TiersHook_TierTransfersPaused({
779
- projectId: PROJECT_ID, tokenId: tokenId, from: from, to: to
776
+ projectId: projectId, tokenId: tokenId, from: from, to: to
780
777
  });
781
778
  }
782
779
  }
@@ -789,11 +786,11 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
789
786
  STORE.recordTransferForTier({tierId: tierId, from: from, to: to});
790
787
 
791
788
  // Deploy the checkpoint module lazily on the first transfer.
792
- if (address(CHECKPOINTS) == address(0)) {
793
- CHECKPOINTS = CHECKPOINTS_DEPLOYER.deploy(address(this));
789
+ if (address(checkpoints) == address(0)) {
790
+ checkpoints = CHECKPOINTS_DEPLOYER.deploy(address(this));
794
791
  }
795
792
 
796
793
  // Notify the checkpoint module to update checkpointed voting power.
797
- CHECKPOINTS.onTransfer({from: from, to: to, tokenId: tokenId});
794
+ checkpoints.onTransfer({from: from, to: to, tokenId: tokenId});
798
795
  }
799
796
  }
@@ -89,7 +89,7 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
89
89
  emit HookDeployed({projectId: projectId, hook: newHook, caller: _msgSender()});
90
90
 
91
91
  newHook.initialize({
92
- projectId: projectId,
92
+ initialProjectId: projectId,
93
93
  name: deployTiersHookConfig.name,
94
94
  symbol: deployTiersHookConfig.symbol,
95
95
  baseUri: deployTiersHookConfig.baseUri,
@@ -89,8 +89,8 @@ contract JB721TiersHookProjectDeployer is
89
89
  returns (uint256 projectId, IJB721TiersHook hook)
90
90
  {
91
91
  // Reserve the project ID up front so permissionless project creations cannot invalidate hook deployment.
92
- IJBProjects PROJECTS = DIRECTORY.PROJECTS();
93
- projectId = PROJECTS.createFor(address(this));
92
+ IJBProjects projects = DIRECTORY.PROJECTS();
93
+ projectId = projects.createFor(address(this));
94
94
 
95
95
  // Deploy the hook.
96
96
  hook = HOOK_DEPLOYER.deployHookFor({
@@ -108,7 +108,7 @@ contract JB721TiersHookProjectDeployer is
108
108
  JBOwnable(address(hook)).transferOwnershipToProject(projectId);
109
109
 
110
110
  // Transfer the project NFT to its intended owner.
111
- PROJECTS.safeTransferFrom({from: address(this), to: owner, tokenId: projectId});
111
+ projects.safeTransferFrom({from: address(this), to: owner, tokenId: projectId});
112
112
  }
113
113
 
114
114
  /// @notice Launches rulesets for a project with an attached 721 tiers hook.
@@ -135,8 +135,8 @@ contract JB721TiersHookProjectDeployer is
135
135
  returns (uint256 rulesetId, IJB721TiersHook hook)
136
136
  {
137
137
  // Get the project's projects contract.
138
- IJBProjects PROJECTS = DIRECTORY.PROJECTS();
139
- address projectOwner = PROJECTS.ownerOf(projectId);
138
+ IJBProjects projects = DIRECTORY.PROJECTS();
139
+ address projectOwner = projects.ownerOf(projectId);
140
140
 
141
141
  // Enforce permissions.
142
142
  _requirePermissionFrom({
@@ -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({
@@ -958,9 +958,8 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
958
958
  revert JB721TiersHookStore_VotingUnitsNotAllowed({tierId: tierId});
959
959
  }
960
960
 
961
- // Make sure the new tier doesn't have a reserve frequency if the 721 contract's flags don't allow it to,
962
- // OR if manual minting is allowed.
963
- if ((flags.noNewTiersWithReserves || tierToAdd.flags.allowOwnerMint) && tierToAdd.reserveFrequency != 0) {
961
+ // Make sure the new tier doesn't have a reserve frequency if the 721 contract's flags don't allow it to.
962
+ if (flags.noNewTiersWithReserves && tierToAdd.reserveFrequency != 0) {
964
963
  revert JB721TiersHookStore_ReserveFrequencyNotAllowed({tierId: tierId});
965
964
  }
966
965
 
@@ -1051,9 +1050,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1051
1050
  }
1052
1051
  }
1053
1052
 
1054
- // Set the `encodedIPFSUri` if needed.
1055
- if (tierToAdd.encodedIPFSUri != bytes32(0)) {
1056
- encodedIPFSUriOf[msg.sender][tierId] = tierToAdd.encodedIPFSUri;
1053
+ // Set the `encodedIpfsUri` if needed.
1054
+ if (tierToAdd.encodedIpfsUri != bytes32(0)) {
1055
+ encodedIpfsUriOf[msg.sender][tierId] = tierToAdd.encodedIpfsUri;
1057
1056
  }
1058
1057
 
1059
1058
  if (startSortedTierId != 0) {
@@ -1392,10 +1391,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
1392
1391
 
1393
1392
  /// @notice Record a new encoded IPFS URI for a tier.
1394
1393
  /// @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.
1394
+ /// @param encodedIpfsUri The encoded IPFS URI to set for the tier.
1396
1395
  // forge-lint: disable-next-line(mixed-case-function)
1397
- function recordSetEncodedIPFSUriOf(uint256 tierId, bytes32 encodedIPFSUri) external override {
1398
- encodedIPFSUriOf[msg.sender][tierId] = encodedIPFSUri;
1396
+ function recordSetEncodedIpfsUriOf(uint256 tierId, bytes32 encodedIpfsUri) external override {
1397
+ encodedIpfsUriOf[msg.sender][tierId] = encodedIpfsUri;
1399
1398
  }
1400
1399
 
1401
1400
  /// @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;
@@ -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({
@@ -190,7 +190,7 @@ contract UnitTestSetup is Test {
190
190
  tiers[i].price = uint104((i + 1) * 10); // The price is `tierId` * 10.
191
191
  tiers[i].initialSupply = uint32(100);
192
192
  tiers[i].reserveBeneficiary = reserveBeneficiary;
193
- tiers[i].encodedIPFSUri = tokenUris[i];
193
+ tiers[i].encodedIpfsUri = tokenUris[i];
194
194
  tiers[i].category = uint24(100);
195
195
  tiers[i].flags.useVotingUnits = true;
196
196
  }
@@ -325,7 +325,7 @@ contract UnitTestSetup is Test {
325
325
  assertEq(first.votingUnits, second.votingUnits);
326
326
  assertEq(first.reserveFrequency, second.reserveFrequency);
327
327
  assertEq(first.reserveBeneficiary, second.reserveBeneficiary);
328
- assertEq(first.encodedIPFSUri, second.encodedIPFSUri);
328
+ assertEq(first.encodedIpfsUri, second.encodedIpfsUri);
329
329
  }
330
330
 
331
331
  // Compare two arrays of `JB721Tier`s.
@@ -339,7 +339,7 @@ contract UnitTestSetup is Test {
339
339
  assertEq(first[i].votingUnits, second[i].votingUnits);
340
340
  assertEq(first[i].reserveFrequency, second[i].reserveFrequency);
341
341
  assertEq(first[i].reserveBeneficiary, second[i].reserveBeneficiary);
342
- assertEq(first[i].encodedIPFSUri, second[i].encodedIPFSUri);
342
+ assertEq(first[i].encodedIpfsUri, second[i].encodedIpfsUri);
343
343
  }
344
344
  }
345
345
 
@@ -390,7 +390,7 @@ contract UnitTestSetup is Test {
390
390
  && first.initialSupply == second.initialSupply && first.votingUnits == second.votingUnits
391
391
  && first.reserveFrequency == second.reserveFrequency
392
392
  && first.reserveBeneficiary == second.reserveBeneficiary
393
- && first.encodedIPFSUri == second.encodedIPFSUri
393
+ && first.encodedIpfsUri == second.encodedIpfsUri
394
394
  && keccak256(abi.encodePacked(first.resolvedUri)) == keccak256(abi.encodePacked(second.resolvedUri)));
395
395
  }
396
396
 
@@ -516,7 +516,7 @@ contract UnitTestSetup is Test {
516
516
  votingUnits: tierConfig.votingUnits,
517
517
  reserveFrequency: tierConfig.reserveFrequency,
518
518
  reserveBeneficiary: reserveBeneficiary,
519
- encodedIPFSUri: i < tokenUris.length ? tokenUris[i] : tokenUris[0],
519
+ encodedIpfsUri: i < tokenUris.length ? tokenUris[i] : tokenUris[0],
520
520
  category: categoryIncrement == 0
521
521
  // forge-lint: disable-next-line(unsafe-typecast)
522
522
  ? tierConfig.category == type(uint24).max ? uint24(i * 2 + 1) : tierConfig.category
@@ -545,7 +545,7 @@ contract UnitTestSetup is Test {
545
545
  votingUnits: tierConfigs[i].votingUnits,
546
546
  reserveFrequency: tierConfigs[i].reserveFrequency,
547
547
  reserveBeneficiary: tierConfigs[i].reserveBeneficiary,
548
- encodedIPFSUri: tierConfigs[i].encodedIPFSUri,
548
+ encodedIpfsUri: tierConfigs[i].encodedIpfsUri,
549
549
  category: tierConfigs[i].category,
550
550
  discountPercent: tierConfigs[i].discountPercent,
551
551
  flags: JB721TierFlags({
@@ -556,7 +556,7 @@ contract UnitTestSetup is Test {
556
556
  cantBuyWithCredits: tierConfigs[i].flags.cantBuyWithCredits
557
557
  }),
558
558
  splitPercent: tierConfigs[i].splitPercent,
559
- resolvedUri: defaultTierConfig.encodedIPFSUri == bytes32(0)
559
+ resolvedUri: defaultTierConfig.encodedIpfsUri == bytes32(0)
560
560
  ? ""
561
561
  : string(abi.encodePacked("resolverURI", _generateTokenId(initialId + i + 1, 0)))
562
562
  });
@@ -721,7 +721,7 @@ contract UnitTestSetup is Test {
721
721
  votingUnits: uint16(0),
722
722
  reserveFrequency: uint16(0),
723
723
  reserveBeneficiary: reserveBeneficiary,
724
- encodedIPFSUri: tokenUris[i],
724
+ encodedIpfsUri: tokenUris[i],
725
725
  category: uint24(100),
726
726
  discountPercent: uint8(0),
727
727
  flags: JB721TierConfigFlags({