@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.
- package/README.md +32 -28
- package/SKILLS.md +120 -27
- package/package.json +5 -5
- package/src/JB721TiersHook.sol +37 -162
- package/src/JB721TiersHookStore.sol +8 -0
- package/src/abstract/JB721Hook.sol +264 -0
- package/src/interfaces/IJB721Hook.sol +21 -0
- package/src/interfaces/IJB721TiersHook.sol +2 -17
- package/src/interfaces/IJB721TiersHookStore.sol +7 -0
- package/src/libraries/JB721TiersHookLib.sol +21 -9
- package/src/structs/JB721TierConfig.sol +3 -1
- package/test/regression/L34_ReserveBeneficiaryOverwrite.t.sol +154 -0
- package/test/regression/L35_CacheTierLookup.t.sol +188 -0
- package/test/regression/L36_SplitNoBeneficiary.t.sol +146 -0
- package/test/unit/M6_TierSupplyCheck.t.sol +1 -1
- package/test/unit/pay_Unit.t.sol +1 -1
- package/test/unit/redeem_Unit.t.sol +4 -6
package/src/JB721TiersHook.sol
CHANGED
|
@@ -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 {
|
|
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,
|
|
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.
|
|
240
|
-
///
|
|
241
|
-
/// @
|
|
242
|
-
/// @
|
|
243
|
-
/// @return
|
|
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
|
|
267
|
-
return interfaceId == type(IJB721TiersHook).interfaceId || 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
|
|
302
|
-
|
|
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
|
+
}
|