@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.
- package/package.json +1 -1
- package/src/JB721TiersHook.sol +236 -146
- package/src/JB721TiersHookDeployer.sol +1 -0
- package/src/JB721TiersHookStore.sol +17 -4
- package/src/interfaces/IJB721TiersHook.sol +17 -2
- package/src/libraries/JB721TiersHookLib.sol +336 -0
- package/src/structs/JB721Tier.sol +3 -0
- package/src/structs/JB721TierConfig.sol +8 -0
- package/src/structs/JBStored721Tier.sol +5 -4
- package/test/721HookAttacks.t.sol +6 -2
- package/test/E2E/Pay_Mint_Redeem_E2E.t.sol +6 -2
- package/test/invariants/handlers/TierLifecycleHandler.sol +3 -1
- package/test/invariants/handlers/TierStoreHandler.sol +4 -1
- package/test/unit/adjustTier_Unit.t.sol +75 -22
- package/test/unit/getters_constructor_Unit.t.sol +14 -9
- package/test/unit/mintFor_mintReservesFor_Unit.t.sol +12 -12
- package/test/unit/pay_Unit.t.sol +6 -10
- package/test/unit/redeem_Unit.t.sol +13 -11
- package/test/unit/tierSplitRouting_Unit.t.sol +275 -0
- package/src/abstract/JB721Hook.sol +0 -279
- package/src/interfaces/IJB721Hook.sol +0 -21
package/package.json
CHANGED
package/src/JB721TiersHook.sol
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
pragma solidity 0.8.23;
|
|
3
3
|
|
|
4
|
+
import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
|
|
4
5
|
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
6
|
+
import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
|
|
5
7
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
6
8
|
import {IJBPrices} from "@bananapus/core-v6/src/interfaces/IJBPrices.sol";
|
|
9
|
+
import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
|
|
7
10
|
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";
|
|
8
13
|
import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
|
|
9
14
|
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
15
|
+
import {JBAfterCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterCashOutRecordedContext.sol";
|
|
10
16
|
import {JBAfterPayRecordedContext} from "@bananapus/core-v6/src/structs/JBAfterPayRecordedContext.sol";
|
|
11
17
|
import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
|
|
18
|
+
import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
|
|
19
|
+
import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
|
|
20
|
+
import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
|
|
12
21
|
import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
|
|
13
22
|
import {JBOwnable} from "@bananapus/ownable-v6/src/JBOwnable.sol";
|
|
14
23
|
import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
|
|
24
|
+
import {IERC2981} from "@openzeppelin/contracts/interfaces/IERC2981.sol";
|
|
15
25
|
import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol";
|
|
16
26
|
import {Context} from "@openzeppelin/contracts/utils/Context.sol";
|
|
17
27
|
import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
import {JB721Hook} from "./abstract/JB721Hook.sol";
|
|
28
|
+
import {ERC721} from "./abstract/ERC721.sol";
|
|
21
29
|
import {IJB721TiersHook} from "./interfaces/IJB721TiersHook.sol";
|
|
22
30
|
import {IJB721TiersHookStore} from "./interfaces/IJB721TiersHookStore.sol";
|
|
23
31
|
import {IJB721TokenUriResolver} from "./interfaces/IJB721TokenUriResolver.sol";
|
|
32
|
+
import {JB721TiersHookLib} from "./libraries/JB721TiersHookLib.sol";
|
|
24
33
|
import {JB721TiersRulesetMetadataResolver} from "./libraries/JB721TiersRulesetMetadataResolver.sol";
|
|
25
34
|
import {JBIpfsDecoder} from "./libraries/JBIpfsDecoder.sol";
|
|
26
35
|
import {JB721Tier} from "./structs/JB721Tier.sol";
|
|
@@ -35,23 +44,33 @@ import {JB721TiersMintReservesConfig} from "./structs/JB721TiersMintReservesConf
|
|
|
35
44
|
/// the project is paid, the hook may mint NFTs to the payer, depending on the hook's setup, the amount paid, and
|
|
36
45
|
/// information specified by the payer. The project's owner can enable NFT cash outs through this hook, allowing
|
|
37
46
|
/// holders to burn their NFTs to reclaim funds from the project (in proportion to the NFT's price).
|
|
38
|
-
contract JB721TiersHook is JBOwnable, ERC2771Context,
|
|
47
|
+
contract JB721TiersHook is JBOwnable, ERC2771Context, ERC721, IJB721TiersHook {
|
|
39
48
|
//*********************************************************************//
|
|
40
49
|
// --------------------------- custom errors ------------------------- //
|
|
41
50
|
//*********************************************************************//
|
|
42
51
|
|
|
43
52
|
error JB721TiersHook_AlreadyInitialized(uint256 projectId);
|
|
44
53
|
error JB721TiersHook_CurrencyMismatch(uint256 paymentCurrency, uint256 tierCurrency);
|
|
54
|
+
error JB721TiersHook_InvalidCashOut();
|
|
55
|
+
error JB721TiersHook_InvalidPay();
|
|
45
56
|
error JB721TiersHook_InvalidPricingDecimals(uint256 decimals);
|
|
46
57
|
error JB721TiersHook_MintReserveNftsPaused();
|
|
47
58
|
error JB721TiersHook_NoProjectId();
|
|
48
59
|
error JB721TiersHook_Overspending(uint256 leftoverAmount);
|
|
49
60
|
error JB721TiersHook_TierTransfersPaused();
|
|
61
|
+
error JB721TiersHook_UnauthorizedToken(uint256 tokenId, address holder);
|
|
62
|
+
error JB721TiersHook_UnexpectedTokenCashedOut();
|
|
50
63
|
|
|
51
64
|
//*********************************************************************//
|
|
52
65
|
// --------------- public immutable stored properties ---------------- //
|
|
53
66
|
//*********************************************************************//
|
|
54
67
|
|
|
68
|
+
/// @notice The directory of terminals and controllers for projects.
|
|
69
|
+
IJBDirectory public immutable override DIRECTORY;
|
|
70
|
+
|
|
71
|
+
/// @notice The ID used when parsing metadata.
|
|
72
|
+
address public immutable override METADATA_ID_TARGET;
|
|
73
|
+
|
|
55
74
|
/// @notice The contract storing and managing project rulesets.
|
|
56
75
|
IJBRulesets public immutable override RULESETS;
|
|
57
76
|
|
|
@@ -61,6 +80,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
61
80
|
//*********************************************************************//
|
|
62
81
|
// ---------------------- public stored properties ------------------- //
|
|
63
82
|
//*********************************************************************//
|
|
83
|
+
|
|
84
|
+
/// @notice The ID of the project that this contract is associated with.
|
|
85
|
+
uint256 public override PROJECT_ID;
|
|
86
|
+
|
|
64
87
|
/// @notice The base URI for the NFT `tokenUris`.
|
|
65
88
|
string public override baseURI;
|
|
66
89
|
|
|
@@ -105,9 +128,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
105
128
|
address trustedForwarder
|
|
106
129
|
)
|
|
107
130
|
JBOwnable(permissions, directory.PROJECTS(), msg.sender, uint88(0))
|
|
108
|
-
JB721Hook(directory)
|
|
109
131
|
ERC2771Context(trustedForwarder)
|
|
110
132
|
{
|
|
133
|
+
DIRECTORY = directory;
|
|
134
|
+
METADATA_ID_TARGET = address(this);
|
|
111
135
|
RULESETS = rulesets;
|
|
112
136
|
STORE = store;
|
|
113
137
|
}
|
|
@@ -116,6 +140,61 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
116
140
|
// ------------------------- external views -------------------------- //
|
|
117
141
|
//*********************************************************************//
|
|
118
142
|
|
|
143
|
+
/// @notice The data calculated before a cash out is recorded in the terminal store. This data is provided to the
|
|
144
|
+
/// terminal's `cashOutTokensOf(...)` transaction.
|
|
145
|
+
/// @dev Sets this contract as the cash out hook. Part of `IJBRulesetDataHook`.
|
|
146
|
+
/// @dev This function is used for NFT cash outs, and will only be called if the project's ruleset has
|
|
147
|
+
/// `useDataHookForCashOut` set to `true`.
|
|
148
|
+
/// @param context The cash out context passed to this contract by the `cashOutTokensOf(...)` function.
|
|
149
|
+
/// @return cashOutTaxRate The cash out tax rate influencing the reclaim amount.
|
|
150
|
+
/// @return cashOutCount The amount of tokens that should be considered cashed out.
|
|
151
|
+
/// @return totalSupply The total amount of tokens that are considered to be existing.
|
|
152
|
+
/// @return hookSpecifications The amount and data to send to cash out hooks (this contract) instead of returning to
|
|
153
|
+
/// the beneficiary.
|
|
154
|
+
function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
|
|
155
|
+
public
|
|
156
|
+
view
|
|
157
|
+
virtual
|
|
158
|
+
override
|
|
159
|
+
returns (
|
|
160
|
+
uint256 cashOutTaxRate,
|
|
161
|
+
uint256 cashOutCount,
|
|
162
|
+
uint256 totalSupply,
|
|
163
|
+
JBCashOutHookSpecification[] memory hookSpecifications
|
|
164
|
+
)
|
|
165
|
+
{
|
|
166
|
+
// Make sure (fungible) project tokens aren't also being cashed out.
|
|
167
|
+
if (context.cashOutCount > 0) revert JB721TiersHook_UnexpectedTokenCashedOut();
|
|
168
|
+
|
|
169
|
+
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
170
|
+
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
171
|
+
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}), metadata: context.metadata
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Use this contract as the only cash out hook.
|
|
175
|
+
hookSpecifications = new JBCashOutHookSpecification[](1);
|
|
176
|
+
hookSpecifications[0] = JBCashOutHookSpecification({hook: this, amount: 0, metadata: bytes("")});
|
|
177
|
+
|
|
178
|
+
uint256[] memory decodedTokenIds;
|
|
179
|
+
|
|
180
|
+
// Decode the metadata.
|
|
181
|
+
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
182
|
+
|
|
183
|
+
// Use the cash out weight of the provided 721s.
|
|
184
|
+
cashOutCount = STORE.cashOutWeightOf({hook: address(this), tokenIds: decodedTokenIds});
|
|
185
|
+
|
|
186
|
+
// Use the total cash out weight of the 721s.
|
|
187
|
+
totalSupply = STORE.totalCashOutWeight(address(this));
|
|
188
|
+
|
|
189
|
+
// Use the cash out tax rate from the context.
|
|
190
|
+
cashOutTaxRate = context.cashOutTaxRate;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// @notice Required by the IJBRulesetDataHook interfaces. Return false to not leak any permissions.
|
|
194
|
+
function hasMintPermissionFor(uint256, JBRuleset memory, address) external pure returns (bool) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
119
198
|
/// @notice The first owner of an NFT.
|
|
120
199
|
/// @dev This is generally the address which paid for the NFT.
|
|
121
200
|
/// @param tokenId The token ID of the NFT to get the first owner of.
|
|
@@ -158,7 +237,40 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
158
237
|
return STORE.balanceOf({hook: address(this), owner: owner});
|
|
159
238
|
}
|
|
160
239
|
|
|
161
|
-
/// @notice
|
|
240
|
+
/// @notice The data calculated before a payment is recorded in the terminal store. This data is provided to the
|
|
241
|
+
/// terminal's `pay(...)` transaction.
|
|
242
|
+
/// @dev Sets this contract as the pay hook. Part of `IJBRulesetDataHook`.
|
|
243
|
+
/// @param context The payment context passed to this contract by the `pay(...)` function.
|
|
244
|
+
/// @return weight The new `weight` to use, overriding the ruleset's `weight`.
|
|
245
|
+
/// @return hookSpecifications The amount and data to send to pay hooks (this contract) instead of adding to the
|
|
246
|
+
/// terminal's balance.
|
|
247
|
+
function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
|
|
248
|
+
public
|
|
249
|
+
view
|
|
250
|
+
virtual
|
|
251
|
+
override
|
|
252
|
+
returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
|
|
253
|
+
{
|
|
254
|
+
weight = context.weight;
|
|
255
|
+
hookSpecifications = new JBPayHookSpecification[](1);
|
|
256
|
+
|
|
257
|
+
// Calculate per-tier split amounts via the library.
|
|
258
|
+
(uint256 totalSplitAmount, bytes memory splitMetadata) =
|
|
259
|
+
JB721TiersHookLib.calculateSplitAmounts(STORE, address(this), METADATA_ID_TARGET, context.metadata);
|
|
260
|
+
|
|
261
|
+
hookSpecifications[0] = JBPayHookSpecification({hook: this, amount: totalSplitAmount, metadata: splitMetadata});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/// @notice Indicates if this contract adheres to the specified interface.
|
|
265
|
+
/// @dev See {IERC165-supportsInterface}.
|
|
266
|
+
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
267
|
+
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC721, IERC165) returns (bool) {
|
|
268
|
+
return interfaceId == type(IJB721TiersHook).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
|
|
269
|
+
|| interfaceId == type(IJBPayHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId
|
|
270
|
+
|| interfaceId == type(IERC2981).interfaceId || super.supportsInterface(interfaceId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// @notice Initializes a cloned copy of the original hook contract.
|
|
162
274
|
/// @param projectId The ID of the project this this hook is associated with.
|
|
163
275
|
/// @param name The name of the NFT collection.
|
|
164
276
|
/// @param symbol The symbol representing the NFT collection.
|
|
@@ -187,8 +299,9 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
187
299
|
// Make sure a projectId is provided.
|
|
188
300
|
if (projectId == 0) revert JB721TiersHook_NoProjectId();
|
|
189
301
|
|
|
190
|
-
// Initialize the
|
|
191
|
-
|
|
302
|
+
// Initialize ERC721 and set the project ID.
|
|
303
|
+
ERC721._initialize({name_: name, symbol_: symbol});
|
|
304
|
+
PROJECT_ID = projectId;
|
|
192
305
|
|
|
193
306
|
// Validate pricing decimals are within a reasonable range.
|
|
194
307
|
if (tiersConfig.decimals > 18) revert JB721TiersHook_InvalidPricingDecimals(tiersConfig.decimals);
|
|
@@ -230,31 +343,6 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
230
343
|
_transferOwnership(_msgSender());
|
|
231
344
|
}
|
|
232
345
|
|
|
233
|
-
/// @notice The combined cash out weight of the NFTs with the specified token IDs.
|
|
234
|
-
/// @dev An NFT's cash out weight is its price.
|
|
235
|
-
/// @dev To get their relative cash out weight, divide the result by the `totalCashOutWeight(...)`.
|
|
236
|
-
/// @param tokenIds The token IDs of the NFTs to get the cumulative cash out weight of.
|
|
237
|
-
/// @return weight The cash out weight of the tokenIds.
|
|
238
|
-
function cashOutWeightOf(
|
|
239
|
-
uint256[] memory tokenIds,
|
|
240
|
-
JBBeforeCashOutRecordedContext calldata
|
|
241
|
-
)
|
|
242
|
-
public
|
|
243
|
-
view
|
|
244
|
-
virtual
|
|
245
|
-
override
|
|
246
|
-
returns (uint256)
|
|
247
|
-
{
|
|
248
|
-
return STORE.cashOutWeightOf({hook: address(this), tokenIds: tokenIds});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/// @notice Indicates if this contract adheres to the specified interface.
|
|
252
|
-
/// @dev See {IERC165-supportsInterface}.
|
|
253
|
-
/// @param interfaceId The ID of the interface to check for adherence to.
|
|
254
|
-
function supportsInterface(bytes4 interfaceId) public view override(IERC165, JB721Hook) returns (bool) {
|
|
255
|
-
return interfaceId == type(IJB721TiersHook).interfaceId || JB721Hook.supportsInterface(interfaceId);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
346
|
/// @notice The metadata URI of the NFT with the specified token ID.
|
|
259
347
|
/// @dev Defers to the `tokenUriResolver` if it is set. Otherwise, use the `tokenUri` corresponding with the NFT's
|
|
260
348
|
/// tier.
|
|
@@ -274,51 +362,75 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
274
362
|
});
|
|
275
363
|
}
|
|
276
364
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
365
|
+
//*********************************************************************//
|
|
366
|
+
// ---------------------- external transactions ---------------------- //
|
|
367
|
+
//*********************************************************************//
|
|
368
|
+
|
|
369
|
+
/// @notice Mints one or more NFTs to the `context.beneficiary` upon payment if conditions are met. Part of
|
|
370
|
+
/// `IJBPayHook`.
|
|
371
|
+
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
372
|
+
/// @param context The payment context passed in by the terminal.
|
|
373
|
+
// slither-disable-next-line locked-ether
|
|
374
|
+
function afterPayRecordedWith(JBAfterPayRecordedContext calldata context) external payable virtual override {
|
|
375
|
+
uint256 projectId = PROJECT_ID;
|
|
376
|
+
|
|
377
|
+
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
378
|
+
// interaction with the correct project.
|
|
379
|
+
if (!DIRECTORY.isTerminalOf(projectId, IJBTerminal(msg.sender)) || context.projectId != projectId) {
|
|
380
|
+
revert JB721TiersHook_InvalidPay();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Process the payment.
|
|
384
|
+
_processPayment(context);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// @notice Burns the specified NFTs upon token holder cash out, reclaiming funds from the project's balance for
|
|
388
|
+
/// `context.beneficiary`. Part of `IJBCashOutHook`.
|
|
389
|
+
/// @dev Reverts if the calling contract is not one of the project's terminals.
|
|
390
|
+
/// @param context The cash out context passed in by the terminal.
|
|
391
|
+
// slither-disable-next-line locked-ether
|
|
392
|
+
function afterCashOutRecordedWith(JBAfterCashOutRecordedContext calldata context)
|
|
393
|
+
external
|
|
394
|
+
payable
|
|
283
395
|
virtual
|
|
284
396
|
override
|
|
285
|
-
returns (uint256)
|
|
286
397
|
{
|
|
287
|
-
|
|
288
|
-
|
|
398
|
+
// Keep a reference to the project ID.
|
|
399
|
+
uint256 projectId = PROJECT_ID;
|
|
289
400
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
401
|
+
// Make sure the caller is a terminal of the project, and that the call is being made on behalf of an
|
|
402
|
+
// interaction with the correct project.
|
|
403
|
+
if (
|
|
404
|
+
msg.value != 0 || !DIRECTORY.isTerminalOf({projectId: projectId, terminal: IJBTerminal(msg.sender)})
|
|
405
|
+
|| context.projectId != projectId
|
|
406
|
+
) revert JB721TiersHook_InvalidCashOut();
|
|
407
|
+
|
|
408
|
+
// Fetch the cash out hook metadata using the corresponding metadata ID.
|
|
409
|
+
(bool metadataExists, bytes memory metadata) = JBMetadataResolver.getDataFor({
|
|
410
|
+
id: JBMetadataResolver.getId({purpose: "cashOut", target: METADATA_ID_TARGET}),
|
|
411
|
+
metadata: context.cashOutMetadata
|
|
412
|
+
});
|
|
293
413
|
|
|
294
|
-
|
|
295
|
-
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
296
|
-
return super._contextSuffixLength();
|
|
297
|
-
}
|
|
414
|
+
uint256[] memory decodedTokenIds;
|
|
298
415
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
/// @return The project's current ruleset.
|
|
302
|
-
function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) {
|
|
303
|
-
// slither-disable-next-line calls-loop
|
|
304
|
-
return RULESETS.currentOf(projectId);
|
|
305
|
-
}
|
|
416
|
+
// Decode the metadata.
|
|
417
|
+
if (metadataExists) decodedTokenIds = abi.decode(metadata, (uint256[]));
|
|
306
418
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
419
|
+
// Iterate through the NFTs, burning them if the owner is correct.
|
|
420
|
+
for (uint256 i; i < decodedTokenIds.length; i++) {
|
|
421
|
+
// Set the current NFT's token ID.
|
|
422
|
+
uint256 tokenId = decodedTokenIds[i];
|
|
312
423
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
316
|
-
return ERC2771Context._msgSender();
|
|
317
|
-
}
|
|
424
|
+
// Make sure the token's owner is correct.
|
|
425
|
+
if (_ownerOf(tokenId) != context.holder) revert JB721TiersHook_UnauthorizedToken(tokenId, context.holder);
|
|
318
426
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
427
|
+
// Burn the token.
|
|
428
|
+
_burn(tokenId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Add to burned counter.
|
|
432
|
+
STORE.recordBurn(decodedTokenIds);
|
|
433
|
+
}
|
|
322
434
|
|
|
323
435
|
/// @notice Add or delete tiers.
|
|
324
436
|
/// @dev Only the contract's owner or an operator with the `ADJUST_TIERS` permission from the owner can adjust the
|
|
@@ -332,28 +444,10 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
332
444
|
account: owner(), projectId: PROJECT_ID, permissionId: JBPermissionIds.ADJUST_721_TIERS
|
|
333
445
|
});
|
|
334
446
|
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
emit RemoveTier({tierId: tierIdsToRemove[i], caller: _msgSender()});
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Record the removed tiers.
|
|
343
|
-
// slither-disable-next-line reentrancy-events
|
|
344
|
-
STORE.recordRemoveTierIds(tierIdsToRemove);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Add the tiers.
|
|
348
|
-
if (tiersToAdd.length != 0) {
|
|
349
|
-
// Record the added tiers in the store.
|
|
350
|
-
uint256[] memory tierIdsAdded = STORE.recordAddTiers(tiersToAdd);
|
|
351
|
-
|
|
352
|
-
// Emit events for each added tier.
|
|
353
|
-
for (uint256 i; i < tiersToAdd.length; i++) {
|
|
354
|
-
emit AddTier({tierId: tierIdsAdded[i], tier: tiersToAdd[i], caller: _msgSender()});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
447
|
+
// Delegate to the library (via DELEGATECALL) for tier removal, addition, event emission, and split setting.
|
|
448
|
+
JB721TiersHookLib.adjustTiersFor(
|
|
449
|
+
STORE, DIRECTORY, PROJECT_ID, address(this), _msgSender(), tiersToAdd, tierIdsToRemove
|
|
450
|
+
);
|
|
357
451
|
}
|
|
358
452
|
|
|
359
453
|
/// @notice Manually mint NFTs from the provided tiers .
|
|
@@ -522,16 +616,38 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
522
616
|
}
|
|
523
617
|
|
|
524
618
|
//*********************************************************************//
|
|
525
|
-
//
|
|
619
|
+
// -------------------------- internal views ------------------------- //
|
|
526
620
|
//*********************************************************************//
|
|
527
621
|
|
|
528
|
-
/// @
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
622
|
+
/// @dev ERC-2771 specifies the context as being a single address (20 bytes).
|
|
623
|
+
function _contextSuffixLength() internal view virtual override(ERC2771Context, Context) returns (uint256) {
|
|
624
|
+
return super._contextSuffixLength();
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/// @notice The project's current ruleset.
|
|
628
|
+
/// @param projectId The ID of the project to check.
|
|
629
|
+
/// @return The project's current ruleset.
|
|
630
|
+
function _currentRulesetOf(uint256 projectId) internal view returns (JBRuleset memory) {
|
|
631
|
+
// slither-disable-next-line calls-loop
|
|
632
|
+
return RULESETS.currentOf(projectId);
|
|
533
633
|
}
|
|
534
634
|
|
|
635
|
+
/// @notice Returns the calldata, preferred to use over `msg.data`
|
|
636
|
+
/// @return calldata the `msg.data` of this call
|
|
637
|
+
function _msgData() internal view override(ERC2771Context, Context) returns (bytes calldata) {
|
|
638
|
+
return ERC2771Context._msgData();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/// @notice Returns the sender, preferred to use over `msg.sender`
|
|
642
|
+
/// @return sender the sender address of this call.
|
|
643
|
+
function _msgSender() internal view override(ERC2771Context, Context) returns (address sender) {
|
|
644
|
+
return ERC2771Context._msgSender();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
//*********************************************************************//
|
|
648
|
+
// ------------------------ internal functions ----------------------- //
|
|
649
|
+
//*********************************************************************//
|
|
650
|
+
|
|
535
651
|
/// @notice Mints one NFT from each of the specified tiers for the beneficiary.
|
|
536
652
|
/// @dev The same tier can be specified more than once.
|
|
537
653
|
/// @param amount The amount to base the mints on. The total price of the NFTs being minted cannot be larger than
|
|
@@ -581,36 +697,19 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
581
697
|
/// the payer's existing credits are NOT applied to the mint. Only the beneficiary's credits are combined with
|
|
582
698
|
/// the incoming payment value. Leftover funds after minting are stored as credits for the beneficiary.
|
|
583
699
|
/// @param context Payment context provided by the terminal after it has recorded the payment in the terminal store.
|
|
584
|
-
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual
|
|
700
|
+
function _processPayment(JBAfterPayRecordedContext calldata context) internal virtual {
|
|
585
701
|
// Normalize the payment value based on the pricing context.
|
|
586
702
|
uint256 value;
|
|
587
|
-
|
|
588
703
|
{
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
// pricing decimals in bits 32-39 (8 bits).
|
|
599
|
-
uint256 pricingDecimals = uint256(uint8(packed >> 32));
|
|
600
|
-
value = mulDiv(
|
|
601
|
-
context.amount.value,
|
|
602
|
-
10 ** pricingDecimals,
|
|
603
|
-
prices.pricePerUnitOf({
|
|
604
|
-
projectId: PROJECT_ID,
|
|
605
|
-
pricingCurrency: context.amount.currency,
|
|
606
|
-
unitCurrency: pricingCurrency,
|
|
607
|
-
decimals: context.amount.decimals
|
|
608
|
-
})
|
|
609
|
-
);
|
|
610
|
-
} else {
|
|
611
|
-
revert JB721TiersHook_CurrencyMismatch(context.amount.currency, pricingCurrency);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
704
|
+
bool valid;
|
|
705
|
+
(value, valid) = JB721TiersHookLib.normalizePaymentValue(
|
|
706
|
+
_packedPricingContext,
|
|
707
|
+
PROJECT_ID,
|
|
708
|
+
context.amount.value,
|
|
709
|
+
context.amount.currency,
|
|
710
|
+
context.amount.decimals
|
|
711
|
+
);
|
|
712
|
+
if (!valid) return;
|
|
614
713
|
}
|
|
615
714
|
|
|
616
715
|
// Keep a reference to the number of NFT credits the beneficiary already has.
|
|
@@ -662,17 +761,14 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
662
761
|
}
|
|
663
762
|
}
|
|
664
763
|
|
|
665
|
-
// If overspending
|
|
666
|
-
if (leftoverAmount != 0)
|
|
667
|
-
// If overspending isn't allowed, revert.
|
|
668
|
-
if (!allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
764
|
+
// If overspending isn't allowed, revert.
|
|
765
|
+
if (leftoverAmount != 0 && !allowOverspending) revert JB721TiersHook_Overspending(leftoverAmount);
|
|
669
766
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
767
|
+
// Update NFT credits if they changed.
|
|
768
|
+
unchecked {
|
|
769
|
+
uint256 newPayCredits = leftoverAmount + unusedPayCredits;
|
|
674
770
|
|
|
675
|
-
|
|
771
|
+
if (newPayCredits != payCredits) {
|
|
676
772
|
if (newPayCredits > payCredits) {
|
|
677
773
|
emit AddPayCredits({
|
|
678
774
|
amount: newPayCredits - payCredits,
|
|
@@ -680,7 +776,7 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
680
776
|
account: context.beneficiary,
|
|
681
777
|
caller: _msgSender()
|
|
682
778
|
});
|
|
683
|
-
} else
|
|
779
|
+
} else {
|
|
684
780
|
emit UsePayCredits({
|
|
685
781
|
amount: payCredits - newPayCredits,
|
|
686
782
|
newTotalCredits: newPayCredits,
|
|
@@ -689,21 +785,15 @@ contract JB721TiersHook is JBOwnable, ERC2771Context, JB721Hook, IJB721TiersHook
|
|
|
689
785
|
});
|
|
690
786
|
}
|
|
691
787
|
|
|
692
|
-
// Store the new NFT credits for the beneficiary.
|
|
693
788
|
payCreditsOf[context.beneficiary] = newPayCredits;
|
|
694
789
|
}
|
|
695
|
-
|
|
696
|
-
} else if (payCredits != unusedPayCredits) {
|
|
697
|
-
// Emit the change in NFT credits.
|
|
698
|
-
emit UsePayCredits({
|
|
699
|
-
amount: payCredits - unusedPayCredits,
|
|
700
|
-
newTotalCredits: unusedPayCredits,
|
|
701
|
-
account: context.beneficiary,
|
|
702
|
-
caller: _msgSender()
|
|
703
|
-
});
|
|
790
|
+
}
|
|
704
791
|
|
|
705
|
-
|
|
706
|
-
|
|
792
|
+
// Distribute any forwarded funds to tier split groups.
|
|
793
|
+
if (context.hookMetadata.length != 0 && context.forwardedAmount.value != 0) {
|
|
794
|
+
JB721TiersHookLib.distributeAll(
|
|
795
|
+
DIRECTORY, PROJECT_ID, address(this), context.forwardedAmount.token, context.hookMetadata
|
|
796
|
+
);
|
|
707
797
|
}
|
|
708
798
|
}
|
|
709
799
|
|
|
@@ -100,6 +100,7 @@ contract JB721TiersHookDeployer is ERC2771Context, IJB721TiersHookDeployer {
|
|
|
100
100
|
JBOwnable(address(newHook)).transferOwnership(_msgSender());
|
|
101
101
|
|
|
102
102
|
// Increment the nonce.
|
|
103
|
+
// slither-disable-next-line reentrancy-benign
|
|
103
104
|
++_nonce;
|
|
104
105
|
|
|
105
106
|
// Add the hook to the address registry. This contract's nonce starts at 1.
|
|
@@ -139,6 +139,13 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
139
139
|
/// @custom:returns The following tier's ID.
|
|
140
140
|
mapping(address hook => mapping(uint256 tierId => uint256)) internal _tierIdAfter;
|
|
141
141
|
|
|
142
|
+
/// @notice Returns the custom voting units for the provided tier ID on the provided hook.
|
|
143
|
+
/// @dev Only populated when `useVotingUnits` is true. When not set, voting power defaults to the tier's price.
|
|
144
|
+
/// @custom:param hook The address of the 721 contract.
|
|
145
|
+
/// @custom:param tierId The ID of the tier.
|
|
146
|
+
/// @custom:returns The voting units for the tier.
|
|
147
|
+
mapping(address hook => mapping(uint256 tierId => uint32)) internal _tierVotingUnitsOf;
|
|
148
|
+
|
|
142
149
|
//*********************************************************************//
|
|
143
150
|
// ------------------------- external views -------------------------- //
|
|
144
151
|
//*********************************************************************//
|
|
@@ -232,7 +239,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
232
239
|
(,, bool useVotingUnits,,) = _unpackBools(storedTier.packedBools);
|
|
233
240
|
|
|
234
241
|
// Return the address' voting units within the tier.
|
|
235
|
-
return balance * (useVotingUnits ?
|
|
242
|
+
return balance * (useVotingUnits ? _tierVotingUnitsOf[hook][tierId] : storedTier.price);
|
|
236
243
|
}
|
|
237
244
|
|
|
238
245
|
/// @notice Gets an array of currently active 721 tiers for the provided 721 contract.
|
|
@@ -368,7 +375,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
368
375
|
|
|
369
376
|
// Add the voting units for the address' balance in this tier.
|
|
370
377
|
// Use custom voting units if set. Otherwise, use the tier's price.
|
|
371
|
-
units += balance * (useVotingUnits ?
|
|
378
|
+
units += balance * (useVotingUnits ? _tierVotingUnitsOf[hook][i] : storedTier.price);
|
|
372
379
|
}
|
|
373
380
|
}
|
|
374
381
|
|
|
@@ -529,7 +536,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
529
536
|
price: storedTier.price,
|
|
530
537
|
remainingSupply: storedTier.remainingSupply,
|
|
531
538
|
initialSupply: storedTier.initialSupply,
|
|
532
|
-
votingUnits: useVotingUnits ?
|
|
539
|
+
votingUnits: useVotingUnits ? _tierVotingUnitsOf[hook][tierId] : storedTier.price,
|
|
533
540
|
// No reserve frequency if there is no reserve beneficiary.
|
|
534
541
|
reserveFrequency: reserveBeneficiary == address(0) ? 0 : storedTier.reserveFrequency,
|
|
535
542
|
reserveBeneficiary: reserveBeneficiary,
|
|
@@ -540,6 +547,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
540
547
|
transfersPausable: transfersPausable,
|
|
541
548
|
cannotBeRemoved: cannotBeRemoved,
|
|
542
549
|
cannotIncreaseDiscountPercent: cannotIncreaseDiscountPercent,
|
|
550
|
+
splitPercent: storedTier.splitPercent,
|
|
543
551
|
resolvedUri: !includeResolvedUri || tokenUriResolverOf[hook] == IJB721TokenUriResolver(address(0))
|
|
544
552
|
? ""
|
|
545
553
|
: tokenUriResolverOf[hook].tokenUriOf({
|
|
@@ -848,7 +856,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
848
856
|
price: uint104(tierToAdd.price),
|
|
849
857
|
remainingSupply: uint32(tierToAdd.initialSupply),
|
|
850
858
|
initialSupply: uint32(tierToAdd.initialSupply),
|
|
851
|
-
|
|
859
|
+
splitPercent: uint32(tierToAdd.splitPercent),
|
|
852
860
|
reserveFrequency: uint16(tierToAdd.reserveFrequency),
|
|
853
861
|
category: uint24(tierToAdd.category),
|
|
854
862
|
discountPercent: uint8(tierToAdd.discountPercent),
|
|
@@ -861,6 +869,11 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
861
869
|
})
|
|
862
870
|
});
|
|
863
871
|
|
|
872
|
+
// Store voting units in a separate mapping if custom voting units are used.
|
|
873
|
+
if (tierToAdd.useVotingUnits && tierToAdd.votingUnits != 0) {
|
|
874
|
+
_tierVotingUnitsOf[msg.sender][tierId] = uint32(tierToAdd.votingUnits);
|
|
875
|
+
}
|
|
876
|
+
|
|
864
877
|
// If this is the first tier in a new category, store it as the first tier in that category.
|
|
865
878
|
// The `_startingTierIdOfCategory` of the category "0" will always be the same as the `_tierIdAfter` the 0th
|
|
866
879
|
// tier.
|