@bananapus/721-hook-v6 0.0.7 → 0.0.9

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.
@@ -1,31 +1,23 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
- import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
5
4
  import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
6
- import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
7
5
  import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
8
6
  import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
9
7
  import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
10
8
  import {IJBRulesets} from "@bananapus/core-v6/src/interfaces/IJBRulesets.sol";
11
- import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
12
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
13
9
  import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
14
10
  import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
15
- import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
16
11
  import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
17
- import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
18
12
  import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
19
- import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
20
13
  import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
21
14
  import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
22
15
  import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
23
16
  import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
24
- import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
25
17
  import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
26
18
  import {Context} from "@openzeppelin/contracts/utils/Context.sol";
27
19
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
28
- import {ERC721} from "./abstract/ERC721.sol";
20
+ import {JB721Hook} from "./abstract/JB721Hook.sol";
29
21
  import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
30
22
  import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
31
23
  import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
@@ -43,33 +35,23 @@ import {JB721TiersMintReservesConfig} from "./structs/JB721TiersMintReservesConf
43
35
  /// the project is paid, the hook may mint NFTs to the payer, depending on the hook's setup, the amount paid, and
44
36
  /// information specified by the payer. The project's owner can enable NFT cash outs through this hook, allowing
45
37
  /// holders to burn their NFTs to reclaim funds from the project (in proportion to the NFT's price).
46
- contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
38
+ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook {
47
39
  //*********************************************************************//
48
40
  // --------------------------- custom errors ------------------------- //
49
41
  //*********************************************************************//
50
42
 
51
43
  error JB721TiersHook_AlreadyInitialized(uint256 projectId);
52
44
  error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
53
- error JB721TiersHook_InvalidCashOut();
54
- error JB721TiersHook_InvalidPay();
55
45
  error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
56
46
  error JB721TiersHook_MintReserveNftsPaused();
57
47
  error JB721TiersHook_NoProjectId();
58
48
  error JB721TiersHook_Overspending(uint256 leftoverAmount);
59
49
  error JB721TiersHook_TierTransfersPaused();
60
- error JB721TiersHook_UnauthorizedToken(uint256 tokenId, address holder);
61
- error JB721TiersHook_UnexpectedTokenCashedOut();
62
50
 
63
51
  //*********************************************************************//
64
52
  // --------------- public immutable stored properties ---------------- //
65
53
  //*********************************************************************//
66
54
 
67
- /// @notice The directory of terminals and controllers for projects.
68
- IJBDirectory public immutable override DIRECTORY;
69
-
70
- /// @notice The ID used when parsing metadata.
71
- address public immutable override METADATA_ID_TARGET;
72
-
73
55
  /// @notice The contract storing and managing project rulesets.
74
56
  IJBRulesets public immutable override RULESETS;
75
57
 
@@ -80,9 +62,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
80
62
  // ---------------------- public stored properties ------------------- //
81
63
  //*********************************************************************//
82
64
 
83
- /// @notice The ID of the project that this contract is associated with.
84
- uint256 public override PROJECT_ID;
85
-
86
65
  /// @notice The base URI for the NFT `tokenUris`.
87
66
  string public override baseURI;
88
67
 
@@ -127,10 +106,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
127
106
  address trustedForwarder
128
107
  )
129
108
  JBOwnable(permissions, directory.PROJECTS(), msg.sender, uint88(0))
109
+ JB721Hook(directory)
130
110
  ERC2771Context(trustedForwarder)
131
111
  {
132
- DIRECTORY = directory;
133
- METADATA_ID_TARGET = address(this);
134
112
  RULESETS = rulesets;
135
113
  STORE = store;
136
114
  }
@@ -139,61 +117,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
139
117
  // ------------------------- external views -------------------------- //
140
118
  //*********************************************************************//
141
119
 
142
- /// @notice The data calculated before a cash out is recorded in the terminal store. This data is provided to the
143
- /// terminal's `cashOutTokensOf(...)` transaction.
144
- /// @dev Sets this contract as the cash out hook. Part of `IJBRulesetDataHook`.
145
- /// @dev This function is used for NFT cash outs, and will only be called if the project's ruleset has
146
- /// `useDataHookForCashOut` set to `true`.
147
- /// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
148
- /// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
149
- /// @return cashOutCount The amount of tokens that should be considered cashed out.
150
- /// @return totalSupply The total amount of tokens that are considered to be existing.
151
- /// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
152
- /// the beneficiary.
153
- function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
154
- public
155
- view
156
- virtual
157
- override
158
- returns (
159
- uint256 cashOutTaxRate,
160
- uint256 cashOutCount,
161
- uint256 totalSupply,
162
- JBCashOutHookSpecification[] memory hookSpecifications
163
- )
164
- {
165
- // Make sure (fungible) project tokens aren't also being cashed out.
166
- if (context.cashOutCount > 0) revert JB721TiersHook_UnexpectedTokenCashedOut();
167
-
168
- // Fetch the cash out hook metadata using the corresponding metadata ID.
169
- (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
170
- id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}), metadata: context.metadata
171
- });
172
-
173
- // Use this contract as the only cash out hook.
174
- hookSpecifications = new JBCashOutHookSpecification[](1);
175
- hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
176
-
177
- uint256[] memory decodedTokenIds;
178
-
179
- // Decode the metadata.
180
- if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
181
-
182
- // Use the cash out weight of the provided 721s.
183
- cashOutCount = STORE.cashOutWeightOf({hook: address(this), tokenIds: decodedTokenIds});
184
-
185
- // Use the total cash out weight of the 721s.
186
- totalSupply = STORE.totalCashOutWeight(address(this));
187
-
188
- // Use the cash out tax rate from the context.
189
- cashOutTaxRate = context.cashOutTaxRate;
190
- }
191
-
192
- /// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
193
- function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
194
- return false;
195
- }
196
-
197
120
  /// @notice The first owner of an NFT.
198
121
  /// @dev This is generally the address which paid for the NFT.
199
122
  /// @param tokenId The token ID of the NFT to get the first owner of.
@@ -236,18 +159,16 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
236
159
  return STORE.balanceOf({hook: address(this), owner: owner});
237
160
  }
238
161
 
239
- /// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the
240
- /// terminal's `pay(...)` transaction.
241
- /// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`.
242
- /// @param context The payment context passed to this contract by the `pay(...)` function.
243
- /// @return weight The new `weight` to use, overriding the ruleset's `weight`.
244
- /// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the
245
- /// terminal's balance.
162
+ /// @notice The data calculated before a payment is recorded in the terminal store.
163
+ /// @dev Overrides the base to calculate the split amount to forward based on tier split percentages.
164
+ /// @param context The payment context.
165
+ /// @return weight The weight to use for token minting (unchanged from ruleset weight).
166
+ /// @return hookSpecifications The hook specifications, with the split amount to forward.
246
167
  function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
247
168
  public
248
169
  view
249
170
  virtual
250
- override
171
+ override(JB721Hook, IJBRulesetDataHook)
251
172
  returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
252
173
  {
253
174
  weight = context.weight;
@@ -260,13 +181,20 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
260
181
  hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: totalSplitAmount, metadata: splitMetadata});
261
182
  }
262
183
 
184
+ /// @notice The combined cash out weight of the NFTs with the specified token IDs.
185
+ /// @dev An NFT's cash out weight is its price.
186
+ /// @dev To get their relative cash out weight, divide the result by the `totalCashOutWeight(...)`.
187
+ /// @param tokenIds The token IDs of the NFTs to get the cumulative cash out weight of.
188
+ /// @return weight The cash out weight of the tokenIds.
189
+ function cashOutWeightOf(uint256[] memory tokenIds) public view virtual override returns (uint256) {
190
+ return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
191
+ }
192
+
263
193
  /// @notice Indicates if this contract adheres to the specified interface.
264
194
  /// @dev See {IERC165-supportsInterface}.
265
195
  /// @param interfaceId The ID of the interface to check for adherence to.
266
- function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
267
- return interfaceId == type(IJB721TiersHook).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
268
- || interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId
269
- || interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
196
+ function supportsInterface(bytes4 interfaceId) public view override(IERC165, JB721Hook) returns (bool) {
197
+ return interfaceId == type(IJB721TiersHook).interfaceId || JB721Hook.supportsInterface(interfaceId);
270
198
  }
271
199
 
272
200
  /// @notice Initializes a cloned copy of the original hook contract.
@@ -298,9 +226,8 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
298
226
  // Make sure a projectId is provided.
299
227
  if (projectId == 0) revert JB721TiersHook_NoProjectId();
300
228
 
301
- // Initialize ERC721 and set the project ID.
302
- ERC721._initialize({name_: name, symbol_: symbol});
303
- PROJECT_ID = projectId;
229
+ // Initialize the superclass.
230
+ JB721Hook._initialize({projectId: projectId, name: name, symbol: symbol});
304
231
 
305
232
  // Validate pricing decimals are within a reasonable range.
306
233
  if (tiersConfig.decimals > 18) revert JB721TiersHook_InvalidPricingDecimals(tiersConfig.decimals);
@@ -352,76 +279,17 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
352
279
  return JB721TiersHookLib.resolveTokenURI(STORE, address(this), baseURI, tokenId);
353
280
  }
354
281
 
282
+ /// @notice The combined cash out weight of all outstanding NFTs.
283
+ /// @dev An NFT's cash out weight is its price.
284
+ /// @return weight The total cash out weight.
285
+ function totalCashOutWeight() public view virtual override returns (uint256) {
286
+ return STORE.totalCashOutWeight(address(this));
287
+ }
288
+
355
289
  //*********************************************************************//
356
290
  // ---------------------- external transactions ---------------------- //
357
291
  //*********************************************************************//
358
292
 
359
- /// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
360
- /// `IJBPayHook`.
361
- /// @dev Reverts if the calling contract is not one of the project's terminals.
362
- /// @param context The payment context passed in by the terminal.
363
- // slither-disable-next-line locked-ether
364
- function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
365
- uint256 projectId = PROJECT_ID;
366
-
367
- // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
368
- // interaction with the correct project.
369
- if (!DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender)) || context.projectId != projectId) {
370
- revert JB721TiersHook_InvalidPay();
371
- }
372
-
373
- // Process the payment.
374
- _processPayment(context);
375
- }
376
-
377
- /// @notice Burns the specified NFTs upon token holder cash out, reclaiming funds from the project's balance for
378
- /// `context.beneficiary`. Part of `IJBCashOutHook`.
379
- /// @dev Reverts if the calling contract is not one of the project's terminals.
380
- /// @param context The cash out context passed in by the terminal.
381
- // slither-disable-next-line locked-ether
382
- function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
383
- external
384
- payable
385
- virtual
386
- override
387
- {
388
- // Keep a reference to the project ID.
389
- uint256 projectId = PROJECT_ID;
390
-
391
- // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
392
- // interaction with the correct project.
393
- if (
394
- msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
395
- || context.projectId != projectId
396
- ) revert JB721TiersHook_InvalidCashOut();
397
-
398
- // Fetch the cash out hook metadata using the corresponding metadata ID.
399
- (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
400
- id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}),
401
- metadata: context.cashOutMetadata
402
- });
403
-
404
- uint256[] memory decodedTokenIds;
405
-
406
- // Decode the metadata.
407
- if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
408
-
409
- // Iterate through the NFTs, burning them if the owner is correct.
410
- for (uint256 i; i < decodedTokenIds.length; i++) {
411
- // Set the current NFT's token ID.
412
- uint256 tokenId = decodedTokenIds[i];
413
-
414
- // Make sure the token's owner is correct.
415
- if (_ownerOf(tokenId) != context.holder) revert JB721TiersHook_UnauthorizedToken(tokenId, context.holder);
416
-
417
- // Burn the token.
418
- _burn(tokenId);
419
- }
420
-
421
- // Add to burned counter.
422
- STORE.recordBurn(decodedTokenIds);
423
- }
424
-
425
293
  /// @notice Add or delete tiers.
426
294
  /// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the
427
295
  /// tiers.
@@ -638,6 +506,13 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
638
506
  // ------------------------ internal functions ----------------------- //
639
507
  //*********************************************************************//
640
508
 
509
+ /// @notice A function which gets called after NFTs have been cashed out and recorded by the terminal.
510
+ /// @param tokenIds The token IDs of the NFTs that were burned.
511
+ function _didBurn(uint256[] memory tokenIds) internal virtual override {
512
+ // Add to burned counter.
513
+ STORE.recordBurn(tokenIds);
514
+ }
515
+
641
516
  /// @notice Mints one NFT from each of the specified tiers for the beneficiary.
642
517
  /// @dev The same tier can be specified more than once.
643
518
  /// @param amount The amount to base the mints on. The total price of the NFTs being minted cannot be larger than
@@ -687,7 +562,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
687
562
  /// the payer's existing credits are NOT applied to the mint. Only the beneficiary's credits are combined with
688
563
  /// the incoming payment value. Leftover funds after minting are stored as credits for the beneficiary.
689
564
  /// @param context Payment context provided by the terminal after it has recorded the payment in the terminal store.
690
- function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual {
565
+ function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual override {
691
566
  // Normalize the payment value based on the pricing context.
692
567
  uint256 value;
693
568
  {
@@ -763,6 +763,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
763
763
  }
764
764
 
765
765
  /// @notice Record newly added tiers.
766
+ /// @dev WARNING: If any tier in `tiersToAdd` has `useReserveBeneficiaryAsDefault` set to `true`, its
767
+ /// `reserveBeneficiary` will overwrite the hook's global `defaultReserveBeneficiaryOf`. This affects ALL existing
768
+ /// tiers that do not have a tier-specific reserve beneficiary set via `_reserveBeneficiaryOf`. Callers should be
769
+ /// aware of this side effect when using `adjustTiers` to add new tiers.
766
770
  /// @param tiersToAdd The tiers to add.
767
771
  /// @return tierIds The IDs of the tiers being added.
768
772
  function recordAddTiers(JB721TierConfig[] calldata tiersToAdd)
@@ -884,8 +888,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
884
888
  // Set the reserve beneficiary if needed.
885
889
  if (tierToAdd.reserveBeneficiary != address(0) && tierToAdd.reserveFrequency != 0) {
886
890
  if (tierToAdd.useReserveBeneficiaryAsDefault) {
891
+ // WARNING: This overwrites the global default for ALL tiers without a tier-specific beneficiary.
887
892
  if (defaultReserveBeneficiaryOf[msg.sender] != tierToAdd.reserveBeneficiary) {
888
893
  defaultReserveBeneficiaryOf[msg.sender] = tierToAdd.reserveBeneficiary;
894
+ emit SetDefaultReserveBeneficiary({
895
+ hook: msg.sender, newBeneficiary: tierToAdd.reserveBeneficiary, caller: msg.sender
896
+ });
889
897
  }
890
898
  } else {
891
899
  _reserveBeneficiaryOf[msg.sender][tierId] = tierToAdd.reserveBeneficiary;
@@ -0,0 +1,264 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
5
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
6
+ import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
7
+ import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
8
+ import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
9
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
10
+ import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
11
+ import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
12
+ import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
13
+ import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
14
+ import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
15
+ import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
16
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
17
+ import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
18
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
19
+
20
+ import {ERC721} from "./ERC721.sol";
21
+ import {IJB721Hook} from "../interfaces/IJB721Hook.sol";
22
+
23
+ /// @title JB721Hook
24
+ /// @notice When a project which uses this hook is paid, this hook may mint NFTs to the payer, depending on this hook's
25
+ /// setup, the amount paid, and information specified by the payer. The project's owner can enable NFT cash outs
26
+ /// through this hook, allowing the NFT holders to burn their NFTs to reclaim funds from the project (in proportion to
27
+ /// the NFT's price).
28
+ abstract contract JB721Hook is ERC721, IJB721Hook {
29
+ //*********************************************************************//
30
+ // --------------------------- custom errors ------------------------- //
31
+ //*********************************************************************//
32
+
33
+ error JB721Hook_InvalidCashOut();
34
+ error JB721Hook_InvalidPay();
35
+ error JB721Hook_UnauthorizedToken(uint256 tokenId, address holder);
36
+ error JB721Hook_UnexpectedTokenCashedOut();
37
+
38
+ //*********************************************************************//
39
+ // --------------- public immutable stored properties ---------------- //
40
+ //*********************************************************************//
41
+
42
+ /// @notice The directory of terminals and controllers for projects.
43
+ IJBDirectory public immutable override DIRECTORY;
44
+
45
+ /// @notice The ID used when parsing metadata.
46
+ address public immutable override METADATA_ID_TARGET;
47
+
48
+ //*********************************************************************//
49
+ // -------------------- public stored properties --------------------- //
50
+ //*********************************************************************//
51
+
52
+ /// @notice The ID of the project that this contract is associated with.
53
+ uint256 public override PROJECT_ID;
54
+
55
+ //*********************************************************************//
56
+ // -------------------------- constructor ---------------------------- //
57
+ //*********************************************************************//
58
+
59
+ /// @param directory A directory of terminals and controllers for projects.
60
+ constructor(IJBDirectory directory) {
61
+ DIRECTORY = directory;
62
+ // Store the address of the original hook deploy. Clones will each use the address of the instance they're based
63
+ // on.
64
+ METADATA_ID_TARGET = address(this);
65
+ }
66
+
67
+ //*********************************************************************//
68
+ // ------------------------- external views -------------------------- //
69
+ //*********************************************************************//
70
+
71
+ /// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the
72
+ /// terminal's `pay(...)` transaction.
73
+ /// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`.
74
+ /// @param context The payment context passed to this contract by the `pay(...)` function.
75
+ /// @return weight The new `weight` to use, overriding the ruleset's `weight`.
76
+ /// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the
77
+ /// terminal's balance.
78
+ function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
79
+ public
80
+ view
81
+ virtual
82
+ override
83
+ returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
84
+ {
85
+ // Forward the received weight and use this contract as the only pay hook.
86
+ weight = context.weight;
87
+ hookSpecifications = new JBPayHookSpecification[](1);
88
+ hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: 0, metadata: bytes("")});
89
+ }
90
+
91
+ /// @notice The data calculated before a cash out is recorded in the terminal store. This data is provided to the
92
+ /// terminal's `cashOutTokensOf(...)` transaction.
93
+ /// @dev Sets this contract as the cash out hook. Part of `IJBRulesetDataHook`.
94
+ /// @dev This function is used for NFT cash outs, and will only be called if the project's ruleset has
95
+ /// `useDataHookForCashOut` set to `true`.
96
+ /// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
97
+ /// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
98
+ /// @return cashOutCount The amount of tokens that should be considered cashed out.
99
+ /// @return totalSupply The total amount of tokens that are considered to be existing.
100
+ /// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
101
+ /// the beneficiary.
102
+ function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
103
+ public
104
+ view
105
+ virtual
106
+ override
107
+ returns (
108
+ uint256 cashOutTaxRate,
109
+ uint256 cashOutCount,
110
+ uint256 totalSupply,
111
+ JBCashOutHookSpecification[] memory hookSpecifications
112
+ )
113
+ {
114
+ // Make sure (fungible) project tokens aren't also being cashed out.
115
+ if (context.cashOutCount > 0) revert JB721Hook_UnexpectedTokenCashedOut();
116
+
117
+ // Fetch the cash out hook metadata using the corresponding metadata ID.
118
+ (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
119
+ id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}), metadata: context.metadata
120
+ });
121
+
122
+ // Use this contract as the only cash out hook.
123
+ hookSpecifications = new JBCashOutHookSpecification[](1);
124
+ hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
125
+
126
+ uint256[] memory decodedTokenIds;
127
+
128
+ // Decode the metadata.
129
+ if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
130
+
131
+ // Use the cash out weight of the provided 721s.
132
+ cashOutCount = cashOutWeightOf(decodedTokenIds);
133
+
134
+ // Use the total cash out weight of the 721s.
135
+ totalSupply = totalCashOutWeight();
136
+
137
+ // Use the cash out tax rate from the context.
138
+ cashOutTaxRate = context.cashOutTaxRate;
139
+ }
140
+
141
+ /// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
142
+ function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
143
+ return false;
144
+ }
145
+
146
+ //*********************************************************************//
147
+ // -------------------------- public views --------------------------- //
148
+ //*********************************************************************//
149
+
150
+ /// @notice Returns the cumulative cash out weight of the specified token IDs relative to the
151
+ /// `totalCashOutWeight`.
152
+ /// @param tokenIds The NFT token IDs to calculate the cumulative cash out weight of.
153
+ /// @return The cumulative cash out weight of the specified token IDs.
154
+ function cashOutWeightOf(uint256[] memory tokenIds) public view virtual returns (uint256) {
155
+ tokenIds; // Prevents unused var compiler and natspec complaints.
156
+ return 0;
157
+ }
158
+
159
+ /// @notice Indicates if this contract adheres to the specified interface.
160
+ /// @dev See {IERC165-supportsInterface}.
161
+ /// @param interfaceId The ID of the interface to check for adherence to.
162
+ function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
163
+ return interfaceId == type(IJB721Hook).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
164
+ || interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId
165
+ || interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
166
+ }
167
+
168
+ /// @notice Calculates the cumulative cash out weight of all NFT token IDs.
169
+ /// @return The total cumulative cash out weight of all NFT token IDs.
170
+ function totalCashOutWeight() public view virtual returns (uint256) {
171
+ return 0;
172
+ }
173
+
174
+ //*********************************************************************//
175
+ // ---------------------- external transactions ---------------------- //
176
+ //*********************************************************************//
177
+
178
+ /// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
179
+ /// `IJBPayHook`.
180
+ /// @dev Reverts if the calling contract is not one of the project's terminals.
181
+ /// @param context The payment context passed in by the terminal.
182
+ // slither-disable-next-line locked-ether
183
+ function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
184
+ uint256 projectId = PROJECT_ID;
185
+
186
+ // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
187
+ // interaction with the correct project.
188
+ if (!DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender)) || context.projectId != projectId) {
189
+ revert JB721Hook_InvalidPay();
190
+ }
191
+
192
+ // Process the payment.
193
+ _processPayment(context);
194
+ }
195
+
196
+ /// @notice Burns the specified NFTs upon token holder cash out, reclaiming funds from the project's balance for
197
+ /// `context.beneficiary`. Part of `IJBCashOutHook`.
198
+ /// @dev Reverts if the calling contract is not one of the project's terminals.
199
+ /// @param context The cash out context passed in by the terminal.
200
+ // slither-disable-next-line locked-ether
201
+ function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
202
+ external
203
+ payable
204
+ virtual
205
+ override
206
+ {
207
+ // Keep a reference to the project ID.
208
+ uint256 projectId = PROJECT_ID;
209
+
210
+ // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
211
+ // interaction with the correct project.
212
+ if (
213
+ msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
214
+ || context.projectId != projectId
215
+ ) revert JB721Hook_InvalidCashOut();
216
+
217
+ // Fetch the cash out hook metadata using the corresponding metadata ID.
218
+ (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
219
+ id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}),
220
+ metadata: context.cashOutMetadata
221
+ });
222
+
223
+ uint256[] memory decodedTokenIds;
224
+
225
+ // Decode the metadata.
226
+ if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
227
+
228
+ // Iterate through the NFTs, burning them if the owner is correct.
229
+ for (uint256 i; i < decodedTokenIds.length; i++) {
230
+ // Set the current NFT's token ID.
231
+ uint256 tokenId = decodedTokenIds[i];
232
+
233
+ // Make sure the token's owner is correct.
234
+ if (_ownerOf(tokenId) != context.holder) revert JB721Hook_UnauthorizedToken(tokenId, context.holder);
235
+
236
+ // Burn the token.
237
+ _burn(tokenId);
238
+ }
239
+
240
+ // Call the hook.
241
+ _didBurn(decodedTokenIds);
242
+ }
243
+
244
+ //*********************************************************************//
245
+ // ---------------------- internal transactions ---------------------- //
246
+ //*********************************************************************//
247
+
248
+ /// @notice Initializes the contract by associating it with a project and adding ERC721 details.
249
+ /// @param projectId The ID of the project that this contract is associated with.
250
+ /// @param name The name of the NFT collection.
251
+ /// @param symbol The symbol representing the NFT collection.
252
+ function _initialize(uint256 projectId, string memory name, string memory symbol) internal {
253
+ ERC721._initialize({name_: name, symbol_: symbol});
254
+ PROJECT_ID = projectId;
255
+ }
256
+
257
+ /// @notice Executes after NFTs have been burned via cash out.
258
+ /// @param tokenIds The token IDs of the NFTs that were burned.
259
+ function _didBurn(uint256[] memory tokenIds) internal virtual;
260
+
261
+ /// @notice Process a received payment.
262
+ /// @param context The payment context passed in by the terminal.
263
+ function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual;
264
+ }
@@ -0,0 +1,21 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.0;
3
+
4
+ import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
5
+ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
6
+ import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
7
+ import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
8
+
9
+ interface IJB721Hook is IJBRulesetDataHook, IJBPayHook, IJBCashOutHook {
10
+ /// @notice The directory of terminals and controllers for projects.
11
+ /// @return The directory contract.
12
+ function DIRECTORY() external view returns (IJBDirectory);
13
+
14
+ /// @notice The ID used when parsing metadata.
15
+ /// @return The address of the metadata ID target.
16
+ function METADATA_ID_TARGET() external view returns (address);
17
+
18
+ /// @notice The ID of the project that this contract is associated with.
19
+ /// @return The project ID.
20
+ function PROJECT_ID() external view returns (uint256);
21
+ }