@bananapus/721-hook-v6 0.0.1 → 0.0.2

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.
@@ -0,0 +1,275 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "../utils/UnitTestSetup.sol";
5
+ import {IJB721TiersHookStore} from "../../src/interfaces/IJB721TiersHookStore.sol";
6
+ import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
7
+ import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
8
+ import {IJBSplits} from "@bananapus/core-v6/src/interfaces/IJBSplits.sol";
9
+ import {IJBController} from "@bananapus/core-v6/src/interfaces/IJBController.sol";
10
+
11
+ contract Test_TierSplitRouting is UnitTestSetup {
12
+ using stdStorage for StdStorage;
13
+
14
+ address alice = makeAddr("alice");
15
+ address bob = makeAddr("bob");
16
+ address mockSplits = makeAddr("mockSplits");
17
+
18
+ function setUp() public override {
19
+ super.setUp();
20
+ vm.etch(mockSplits, new bytes(0x69));
21
+ }
22
+
23
+ // Helper: build a tier config with splits.
24
+ function _tierConfigWithSplit(
25
+ uint104 price,
26
+ uint32 splitPercent
27
+ )
28
+ internal
29
+ pure
30
+ returns (JB721TierConfig memory config)
31
+ {
32
+ config.price = price;
33
+ config.initialSupply = uint32(100);
34
+ config.category = uint24(1);
35
+ config.encodedIPFSUri = bytes32(uint256(0x1234));
36
+ config.splitPercent = splitPercent;
37
+ }
38
+
39
+ // Helper: build payer metadata for tier IDs.
40
+ function _buildPayerMetadata(
41
+ address hookAddress,
42
+ uint16[] memory tierIdsToMint
43
+ )
44
+ internal
45
+ view
46
+ returns (bytes memory)
47
+ {
48
+ bytes[] memory data = new bytes[](1);
49
+ data[0] = abi.encode(false, tierIdsToMint);
50
+ bytes4[] memory ids = new bytes4[](1);
51
+ ids[0] = metadataHelper.getId("pay", hookAddress);
52
+ return metadataHelper.createMetadata(ids, data);
53
+ }
54
+
55
+ // ──────────────────────────────────────────────
56
+ // Test: beforePayRecordedWith calculates split amount
57
+ // ──────────────────────────────────────────────
58
+
59
+ function test_beforePayRecorded_calculatesSplitAmount() public {
60
+ // Create hook with a default tier (no splits).
61
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
62
+ IJB721TiersHookStore hookStore = testHook.STORE();
63
+
64
+ // Add a tier with 50% split directly to the hook's store.
65
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
66
+ tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000); // 50%
67
+ vm.prank(address(testHook));
68
+ uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
69
+
70
+ // Build payer metadata requesting that tier.
71
+ uint16[] memory mintIds = new uint16[](1);
72
+ mintIds[0] = uint16(tierIds[0]);
73
+ bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
74
+
75
+ JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
76
+ terminal: mockTerminalAddress,
77
+ payer: beneficiary,
78
+ amount: JBTokenAmount({
79
+ token: JBConstants.NATIVE_TOKEN,
80
+ value: 1 ether,
81
+ decimals: 18,
82
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
83
+ }),
84
+ projectId: projectId,
85
+ rulesetId: 0,
86
+ beneficiary: beneficiary,
87
+ weight: 10e18,
88
+ reservedPercent: 5000,
89
+ metadata: payerMetadata
90
+ });
91
+
92
+ (uint256 weight, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
93
+
94
+ // Weight unchanged.
95
+ assertEq(weight, 10e18);
96
+ // Hook spec should forward 50% of 1 ETH = 0.5 ETH.
97
+ assertEq(specs.length, 1);
98
+ assertEq(specs[0].amount, 0.5 ether);
99
+ }
100
+
101
+ // ──────────────────────────────────────────────
102
+ // Test: no splitPercent means no forwarded amount
103
+ // ──────────────────────────────────────────────
104
+
105
+ function test_beforePayRecorded_noSplitPercent_noForwardedAmount() public {
106
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
107
+ IJB721TiersHookStore hookStore = testHook.STORE();
108
+
109
+ // Add a tier with 0% split.
110
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
111
+ tierConfigs[0] = _tierConfigWithSplit(1 ether, 0);
112
+ vm.prank(address(testHook));
113
+ uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
114
+
115
+ uint16[] memory mintIds = new uint16[](1);
116
+ mintIds[0] = uint16(tierIds[0]);
117
+ bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
118
+
119
+ JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
120
+ terminal: mockTerminalAddress,
121
+ payer: beneficiary,
122
+ amount: JBTokenAmount({
123
+ token: JBConstants.NATIVE_TOKEN,
124
+ value: 1 ether,
125
+ decimals: 18,
126
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
127
+ }),
128
+ projectId: projectId,
129
+ rulesetId: 0,
130
+ beneficiary: beneficiary,
131
+ weight: 10e18,
132
+ reservedPercent: 5000,
133
+ metadata: payerMetadata
134
+ });
135
+
136
+ (, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
137
+
138
+ // No split amount forwarded.
139
+ assertEq(specs[0].amount, 0);
140
+ }
141
+
142
+ // ──────────────────────────────────────────────
143
+ // Test: multiple tiers with different split percents
144
+ // ──────────────────────────────────────────────
145
+
146
+ function test_beforePayRecorded_multipleTiersDifferentSplitPercents() public {
147
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
148
+ IJB721TiersHookStore hookStore = testHook.STORE();
149
+
150
+ // Tier 1: 1 ETH, 30% split. Tier 2: 2 ETH, 100% split.
151
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](2);
152
+ tierConfigs[0] = _tierConfigWithSplit(1 ether, 300_000_000);
153
+ tierConfigs[0].category = 1;
154
+ tierConfigs[1] = _tierConfigWithSplit(2 ether, 1_000_000_000);
155
+ tierConfigs[1].category = 2;
156
+ vm.prank(address(testHook));
157
+ uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
158
+
159
+ uint16[] memory mintIds = new uint16[](2);
160
+ mintIds[0] = uint16(tierIds[0]);
161
+ mintIds[1] = uint16(tierIds[1]);
162
+ bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
163
+
164
+ JBBeforePayRecordedContext memory context = JBBeforePayRecordedContext({
165
+ terminal: mockTerminalAddress,
166
+ payer: beneficiary,
167
+ amount: JBTokenAmount({
168
+ token: JBConstants.NATIVE_TOKEN,
169
+ value: 3 ether,
170
+ decimals: 18,
171
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
172
+ }),
173
+ projectId: projectId,
174
+ rulesetId: 0,
175
+ beneficiary: beneficiary,
176
+ weight: 10e18,
177
+ reservedPercent: 5000,
178
+ metadata: payerMetadata
179
+ });
180
+
181
+ (, JBPayHookSpecification[] memory specs) = testHook.beforePayRecordedWith(context);
182
+
183
+ // Total split = 1 ETH * 30% + 2 ETH * 100% = 0.3 + 2.0 = 2.3 ETH.
184
+ assertEq(specs[0].amount, 2.3 ether);
185
+ }
186
+
187
+ // ──────────────────────────────────────────────
188
+ // Test: afterPayRecordedWith distributes to split beneficiary
189
+ // ──────────────────────────────────────────────
190
+
191
+ function test_afterPayRecorded_distributesToBeneficiary() public {
192
+ ForTest_JB721TiersHook testHook = _initializeForTestHook(0);
193
+ IJB721TiersHookStore hookStore = testHook.STORE();
194
+
195
+ // Add a tier with 50% split.
196
+ JB721TierConfig[] memory tierConfigs = new JB721TierConfig[](1);
197
+ tierConfigs[0] = _tierConfigWithSplit(1 ether, 500_000_000);
198
+ vm.prank(address(testHook));
199
+ uint256[] memory tierIds = hookStore.recordAddTiers(tierConfigs);
200
+
201
+ // Mock directory checks.
202
+ mockAndExpect(
203
+ address(mockJBDirectory),
204
+ abi.encodeWithSelector(IJBDirectory.isTerminalOf.selector, projectId, mockTerminalAddress),
205
+ abi.encode(true)
206
+ );
207
+ mockAndExpect(
208
+ address(mockJBDirectory),
209
+ abi.encodeWithSelector(IJBDirectory.controllerOf.selector, projectId),
210
+ abi.encode(mockJBController)
211
+ );
212
+ mockAndExpect(mockJBController, abi.encodeWithSelector(IJBController.SPLITS.selector), abi.encode(mockSplits));
213
+
214
+ // Mock splits: alice gets 100%.
215
+ JBSplit[] memory splits = new JBSplit[](1);
216
+ splits[0] = JBSplit({
217
+ percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
218
+ projectId: 0,
219
+ beneficiary: payable(alice),
220
+ preferAddToBalance: false,
221
+ lockedUntil: 0,
222
+ hook: IJBSplitHook(address(0))
223
+ });
224
+
225
+ uint256 groupId = uint256(uint160(address(testHook))) | (uint256(tierIds[0]) << 160);
226
+ mockAndExpect(
227
+ mockSplits, abi.encodeWithSelector(IJBSplits.splitsOf.selector, projectId, 0, groupId), abi.encode(splits)
228
+ );
229
+
230
+ // Build payer metadata.
231
+ uint16[] memory mintIds = new uint16[](1);
232
+ mintIds[0] = uint16(tierIds[0]);
233
+ bytes memory payerMetadata = _buildPayerMetadata(address(testHook), mintIds);
234
+
235
+ // Build hook metadata (per-tier split breakdown from beforePayRecordedWith).
236
+ uint16[] memory splitTierIds = new uint16[](1);
237
+ splitTierIds[0] = uint16(tierIds[0]);
238
+ uint256[] memory splitAmounts = new uint256[](1);
239
+ splitAmounts[0] = 0.5 ether;
240
+
241
+ JBAfterPayRecordedContext memory payContext = JBAfterPayRecordedContext({
242
+ payer: beneficiary,
243
+ projectId: projectId,
244
+ rulesetId: 0,
245
+ amount: JBTokenAmount({
246
+ token: JBConstants.NATIVE_TOKEN,
247
+ value: 1 ether,
248
+ decimals: 18,
249
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
250
+ }),
251
+ forwardedAmount: JBTokenAmount({
252
+ token: JBConstants.NATIVE_TOKEN,
253
+ value: 0.5 ether,
254
+ decimals: 18,
255
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
256
+ }),
257
+ weight: 10e18,
258
+ newlyIssuedTokenCount: 0,
259
+ beneficiary: beneficiary,
260
+ hookMetadata: abi.encode(splitTierIds, splitAmounts),
261
+ payerMetadata: payerMetadata
262
+ });
263
+
264
+ uint256 aliceBalanceBefore = alice.balance;
265
+
266
+ vm.deal(mockTerminalAddress, 1 ether);
267
+ vm.prank(mockTerminalAddress);
268
+ testHook.afterPayRecordedWith{value: 0.5 ether}(payContext);
269
+
270
+ // Alice should have received 0.5 ETH.
271
+ assertEq(alice.balance - aliceBalanceBefore, 0.5 ether);
272
+ // NFT should have been minted.
273
+ assertEq(testHook.balanceOf(beneficiary), 1);
274
+ }
275
+ }
@@ -1,279 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.23;
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 {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
10
- import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
11
- import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
12
- import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
13
- import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
14
- import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
15
- import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
16
- import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
17
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
18
- import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
19
- import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
20
- import {mulDiv} from "@prb/math/src/Common.sol";
21
-
22
- import {ERC721} from "./ERC721.sol";
23
- import {IJB721Hook} from "../interfaces/IJB721Hook.sol";
24
-
25
- /// @title JB721Hook
26
- /// @notice When a project which uses this hook is paid, this hook may mint NFTs to the payer, depending on this hook's
27
- /// setup, the amount paid, and information specified by the payer. The project's owner can enable NFT cash outs.
28
- /// through this hook, allowing the NFT holders to burn their NFTs to reclaim funds from the project (in proportion to
29
- /// the NFT's price).
30
- abstract contract JB721Hook is ERC721, IJB721Hook {
31
- //*********************************************************************//
32
- // --------------------------- custom errors ------------------------- //
33
- //*********************************************************************//
34
-
35
- error JB721Hook_InvalidCashOut();
36
- error JB721Hook_InvalidPay();
37
- error JB721Hook_UnauthorizedToken(uint256 tokenId, address holder);
38
- error JB721Hook_UnexpectedTokenCashedOut();
39
-
40
- //*********************************************************************//
41
- // --------------- public immutable stored properties ---------------- //
42
- //*********************************************************************//
43
-
44
- /// @notice The directory of terminals and controllers for projects.
45
- IJBDirectory public immutable override DIRECTORY;
46
-
47
- /// @notice The ID used when parsing metadata.
48
- address public immutable override METADATA_ID_TARGET;
49
-
50
- //*********************************************************************//
51
- // -------------------- public stored properties --------------------- //
52
- //*********************************************************************//
53
-
54
- /// @notice The ID of the project that this contract is associated with.
55
- uint256 public override PROJECT_ID;
56
-
57
- //*********************************************************************//
58
- // -------------------------- constructor ---------------------------- //
59
- //*********************************************************************//
60
-
61
- /// @param directory A directory of terminals and controllers for projects.
62
- constructor(IJBDirectory directory) {
63
- DIRECTORY = directory;
64
- // Store the address of the original hook deploy. Clones will each use the address of the instance they're based
65
- // on.
66
- METADATA_ID_TARGET = address(this);
67
- }
68
-
69
- //*********************************************************************//
70
- // ------------------------- external views -------------------------- //
71
- //*********************************************************************//
72
-
73
- /// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the
74
- /// terminal's `pay(...)` transaction.
75
- /// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`.
76
- /// @param context The payment context passed to this contract by the `pay(...)` function.
77
- /// @return weight The new `weight` to use, overriding the ruleset's `weight`.
78
- /// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the
79
- /// terminal's balance.
80
- function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
81
- public
82
- view
83
- virtual
84
- override
85
- returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
86
- {
87
- // Forward the received weight and memo, and use this contract as the only pay hook.
88
- weight = context.weight;
89
- hookSpecifications = new JBPayHookSpecification[](1);
90
- hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: 0, metadata: bytes("")});
91
- }
92
-
93
- /// @notice The data calculated before a cash out is recorded in the terminal store. This data is provided to the
94
- /// terminal's `cashOutTokensOf(...)` transaction.
95
- /// @dev Sets this contract as the cash out hook. Part of `IJBRulesetDataHook`.
96
- /// @dev This function is used for NFT cash outs, and will only be called if the project's ruleset has
97
- /// `useDataHookForCashOut` set to `true`.
98
- /// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
99
- /// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
100
- /// @return cashOutCount The amount of tokens that should be considered cashed out.
101
- /// @return totalSupply The total amount of tokens that are considered to be existing.
102
- /// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
103
- /// the beneficiary.
104
- function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
105
- public
106
- view
107
- virtual
108
- override
109
- returns (
110
- uint256 cashOutTaxRate,
111
- uint256 cashOutCount,
112
- uint256 totalSupply,
113
- JBCashOutHookSpecification[] memory hookSpecifications
114
- )
115
- {
116
- // Make sure (fungible) project tokens aren't also being cashed out.
117
- if (context.cashOutCount > 0) revert JB721Hook_UnexpectedTokenCashedOut();
118
-
119
- // Fetch the cash out hook metadata using the corresponding metadata ID.
120
- (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
121
- id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}), metadata: context.metadata
122
- });
123
-
124
- // Use this contract as the only cash out hook.
125
- hookSpecifications = new JBCashOutHookSpecification[](1);
126
- hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
127
-
128
- uint256[] memory decodedTokenIds;
129
-
130
- // Decode the metadata.
131
- if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
132
-
133
- // Use the cash out weight of the provided 721s.
134
- cashOutCount = cashOutWeightOf({tokenIds: decodedTokenIds, context: context});
135
-
136
- // Use the total cash out weight of the 721s.
137
- totalSupply = totalCashOutWeight(context);
138
-
139
- // Use the cash out tax rate from the context.
140
- cashOutTaxRate = context.cashOutTaxRate;
141
- }
142
-
143
- /// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
144
- function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
145
- return false;
146
- }
147
-
148
- //*********************************************************************//
149
- // -------------------------- public views --------------------------- //
150
- //*********************************************************************//
151
-
152
- /// @notice Returns the cumulative cash out weight of the specified token IDs relative to the
153
- /// `totalCashOutWeight`.
154
- /// @param tokenIds The NFT token IDs to calculate the cumulative cash out weight of.
155
- /// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
156
- /// @return The cumulative cash out weight of the specified token IDs.
157
- function cashOutWeightOf(
158
- uint256[] memory tokenIds,
159
- JBBeforeCashOutRecordedContext calldata context
160
- )
161
- public
162
- view
163
- virtual
164
- returns (uint256)
165
- {
166
- tokenIds; // Prevents unused var compiler and natspec complaints.
167
- context; // Prevents unused var compiler and natspec complaints.
168
- return 0;
169
- }
170
-
171
- /// @notice Indicates if this contract adheres to the specified interface.
172
- /// @dev See {IERC165-supportsInterface}.
173
- /// @param interfaceId The ID of the interface to check for adherence to.
174
- function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
175
- return interfaceId == type(IJB721Hook).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
176
- || interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId
177
- || interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
178
- }
179
-
180
- /// @notice Calculates the cumulative cash out weight of all NFT token IDs.
181
- /// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
182
- /// @return The total cumulative cash out weight of all NFT token IDs.
183
- function totalCashOutWeight(JBBeforeCashOutRecordedContext calldata context) public view virtual returns (uint256) {
184
- context; // Prevents unused var compiler and natspec complaints.
185
- return 0;
186
- }
187
-
188
- //*********************************************************************//
189
- // ---------------------- external transactions ---------------------- //
190
- //*********************************************************************//
191
-
192
- /// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
193
- /// `IJBPayHook`.
194
- /// @dev Reverts if the calling contract is not one of the project's terminals.
195
- /// @param context The payment context passed in by the terminal.
196
- // slither-disable-next-line locked-ether
197
- function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
198
- uint256 projectId = PROJECT_ID;
199
-
200
- // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
201
- // interaction with the correct project.
202
- if (
203
- msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
204
- || context.projectId != projectId
205
- ) revert JB721Hook_InvalidPay();
206
-
207
- // Process the payment.
208
- _processPayment(context);
209
- }
210
-
211
- /// @notice Burns the specified NFTs upon token holder cash out, reclaiming funds from the project's balance for
212
- /// `context.beneficiary`. Part of `IJBCashOutHook`.
213
- /// @dev Reverts if the calling contract is not one of the project's terminals.
214
- /// @param context The cash out context passed in by the terminal.
215
- // slither-disable-next-line locked-ether
216
- function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
217
- external
218
- payable
219
- virtual
220
- override
221
- {
222
- // Keep a reference to the project ID.
223
- uint256 projectId = PROJECT_ID;
224
-
225
- // Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
226
- // interaction with the correct project.
227
- if (
228
- msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
229
- || context.projectId != projectId
230
- ) revert JB721Hook_InvalidCashOut();
231
-
232
- // Fetch the cash out hook metadata using the corresponding metadata ID.
233
- (bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
234
- id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}),
235
- metadata: context.cashOutMetadata
236
- });
237
-
238
- uint256[] memory decodedTokenIds;
239
-
240
- // Decode the metadata.
241
- if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
242
-
243
- // Iterate through the NFTs, burning them if the owner is correct.
244
- for (uint256 i; i < decodedTokenIds.length; i++) {
245
- // Set the current NFT's token ID.
246
- uint256 tokenId = decodedTokenIds[i];
247
-
248
- // Make sure the token's owner is correct.
249
- if (_ownerOf(tokenId) != context.holder) revert JB721Hook_UnauthorizedToken(tokenId, context.holder);
250
-
251
- // Burn the token.
252
- _burn(tokenId);
253
- }
254
-
255
- // Call the hook.
256
- _didBurn(decodedTokenIds);
257
- }
258
-
259
- //*********************************************************************//
260
- // ---------------------- internal transactions ---------------------- //
261
- //*********************************************************************//
262
-
263
- /// @notice Initializes the contract by associating it with a project and adding ERC721 details.
264
- /// @param projectId The ID of the project that this contract is associated with.
265
- /// @param name The name of the NFT collection.
266
- /// @param symbol The symbol representing the NFT collection.
267
- function _initialize(uint256 projectId, string memory name, string memory symbol) internal {
268
- ERC721._initialize({name_: name, symbol_: symbol});
269
- PROJECT_ID = projectId;
270
- }
271
-
272
- /// @notice Executes after NFTs have been burned via cash out.
273
- /// @param tokenIds The token IDs of the NFTs that were burned.
274
- function _didBurn(uint256[] memory tokenIds) internal virtual;
275
-
276
- /// @notice Process a received payment.
277
- /// @param context The payment context passed in by the terminal.
278
- function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual;
279
- }
@@ -1,21 +0,0 @@
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
- }