@bananapus/721-hook-v6 0.0.43 → 0.0.46
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/CHANGELOG.md +2 -3
- package/README.md +2 -2
- package/package.json +2 -2
- package/references/operations.md +2 -2
- package/references/runtime.md +1 -1
- package/script/helpers/Hook721DeploymentLib.sol +21 -5
- package/src/JB721Checkpoints.sol +5 -7
- package/src/JB721CheckpointsDeployer.sol +9 -1
- package/src/JB721TiersHook.sol +72 -76
- package/src/JB721TiersHookDeployer.sol +8 -6
- package/src/JB721TiersHookProjectDeployer.sol +22 -20
- package/src/JB721TiersHookStore.sol +170 -134
- package/src/abstract/ERC721.sol +24 -22
- package/src/abstract/JB721Hook.sol +20 -14
- package/src/interfaces/IJB721Checkpoints.sol +1 -1
- package/src/interfaces/IJB721CheckpointsDeployer.sol +0 -3
- package/src/interfaces/IJB721TiersHook.sol +2 -10
- package/src/interfaces/IJB721TiersHookProjectDeployer.sol +2 -2
- package/src/interfaces/IJB721TiersHookStore.sol +11 -11
- package/src/libraries/JB721Constants.sol +1 -0
- package/src/libraries/JB721TiersHookLib.sol +20 -33
- package/src/libraries/JBBitmap.sol +1 -1
- package/src/structs/JB721TiersHookFlags.sol +1 -1
- package/src/structs/JBPayDataHookRulesetMetadata.sol +3 -3
- package/test/utils/UnitTestSetup.sol +2 -2
|
@@ -16,7 +16,11 @@ import {JBBitmapWord} from "./structs/JBBitmapWord.sol";
|
|
|
16
16
|
import {JBStored721Tier} from "./structs/JBStored721Tier.sol";
|
|
17
17
|
|
|
18
18
|
/// @title JB721TiersHookStore
|
|
19
|
-
/// @notice
|
|
19
|
+
/// @notice The shared data store for all `JB721TiersHook` instances. Stores tier definitions, mint counts, reserve
|
|
20
|
+
/// tracking, voting units, and removal bitmaps. Each hook registers its own tiers here; the store handles
|
|
21
|
+
/// tier validation, minting logic (including reserve accounting), and cash-out weight calculations.
|
|
22
|
+
/// @dev One store is shared across all hooks. Functions are keyed by hook address so each hook reads/writes
|
|
23
|
+
/// only its own data. Tier IDs are sequential per hook and 1-indexed.
|
|
20
24
|
contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
21
25
|
using JBBitmap for mapping(uint256 => uint256);
|
|
22
26
|
using JBBitmap for JBBitmapWord;
|
|
@@ -27,7 +31,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
27
31
|
|
|
28
32
|
error JB721TiersHookStore_CantMintManually(uint256 tierId);
|
|
29
33
|
error JB721TiersHookStore_CantRemoveTier(uint256 tierId);
|
|
30
|
-
error JB721TiersHookStore_DeadlockedReserve();
|
|
34
|
+
error JB721TiersHookStore_DeadlockedReserve(uint256 tierId, uint256 initialSupply, uint256 reserveFrequency);
|
|
31
35
|
error JB721TiersHookStore_DiscountPercentExceedsBounds(uint256 percent, uint256 limit);
|
|
32
36
|
error JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(uint256 percent, uint256 storedPercent);
|
|
33
37
|
error JB721TiersHookStore_InsufficientPendingReserves(uint256 count, uint256 numberOfPendingReserves);
|
|
@@ -155,48 +159,50 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
155
159
|
// ------------------------- external views -------------------------- //
|
|
156
160
|
//*********************************************************************//
|
|
157
161
|
|
|
158
|
-
/// @notice
|
|
159
|
-
///
|
|
160
|
-
/// @param hook The 721 contract
|
|
161
|
-
/// @param tokenId The token ID of the
|
|
162
|
-
/// @return The encoded IPFS URI.
|
|
162
|
+
/// @notice Get the encoded IPFS URI for the tier that a specific NFT belongs to. Used to resolve the NFT's
|
|
163
|
+
/// metadata when no custom `tokenUriResolver` is set.
|
|
164
|
+
/// @param hook The 721 hook contract the NFT belongs to.
|
|
165
|
+
/// @param tokenId The token ID of the NFT.
|
|
166
|
+
/// @return The encoded IPFS URI for the NFT's tier.
|
|
163
167
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
164
168
|
function encodedTierIPFSUriOf(address hook, uint256 tokenId) external view override returns (bytes32) {
|
|
165
169
|
return encodedIPFSUriOf[hook][tierIdOfToken(tokenId)];
|
|
166
170
|
}
|
|
167
171
|
|
|
168
|
-
/// @notice Get the flags
|
|
169
|
-
///
|
|
170
|
-
/// @
|
|
172
|
+
/// @notice Get the behavioral flags for a hook — such as whether transfers are pausable, whether NFT holders can
|
|
173
|
+
/// cash out, and whether token issuance occurs for split-routed payments.
|
|
174
|
+
/// @param hook The 721 hook contract to get the flags of.
|
|
175
|
+
/// @return The hook's flags.
|
|
171
176
|
function flagsOf(address hook) external view override returns (JB721TiersHookFlags memory) {
|
|
172
177
|
return _flagsOf[hook];
|
|
173
178
|
}
|
|
174
179
|
|
|
175
|
-
/// @notice Check
|
|
176
|
-
///
|
|
177
|
-
/// @param
|
|
178
|
-
/// @
|
|
180
|
+
/// @notice Check whether a tier has been removed. Removed tiers can no longer be minted from, but existing NFTs
|
|
181
|
+
/// from that tier remain valid and can still be cashed out.
|
|
182
|
+
/// @param hook The 721 hook contract the tier belongs to.
|
|
183
|
+
/// @param tierId The ID of the tier to check.
|
|
184
|
+
/// @return `true` if the tier has been removed, `false` otherwise.
|
|
179
185
|
function isTierRemoved(address hook, uint256 tierId) external view override returns (bool) {
|
|
180
186
|
JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[hook].readId(tierId);
|
|
181
187
|
|
|
182
188
|
return bitmapWord.isTierIdRemoved(tierId);
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
/// @notice
|
|
186
|
-
///
|
|
187
|
-
///
|
|
188
|
-
/// @param
|
|
189
|
-
/// @
|
|
191
|
+
/// @notice How many reserved NFTs are waiting to be minted for a tier. Reserves accumulate automatically as
|
|
192
|
+
/// non-reserve NFTs are minted (based on the tier's `reserveFrequency`). Anyone can mint them via
|
|
193
|
+
/// `mintPendingReservesFor`.
|
|
194
|
+
/// @param hook The 721 hook contract to check.
|
|
195
|
+
/// @param tierId The ID of the tier to check.
|
|
196
|
+
/// @return The number of pending reserved NFTs that can be minted.
|
|
190
197
|
function numberOfPendingReservesFor(address hook, uint256 tierId) external view override returns (uint256) {
|
|
191
198
|
return _numberOfPendingReservesFor({hook: hook, tierId: tierId, storedTier: _storedTierOf[hook][tierId]});
|
|
192
199
|
}
|
|
193
200
|
|
|
194
|
-
/// @notice
|
|
195
|
-
/// @param hook The 721 contract
|
|
196
|
-
/// @param tokenId The token ID of the
|
|
197
|
-
/// @param includeResolvedUri If
|
|
198
|
-
///
|
|
199
|
-
/// @return The tier.
|
|
201
|
+
/// @notice Look up which tier an NFT belongs to and return the full tier details (price, supply, metadata, etc.).
|
|
202
|
+
/// @param hook The 721 hook contract the NFT belongs to.
|
|
203
|
+
/// @param tokenId The token ID of the NFT.
|
|
204
|
+
/// @param includeResolvedUri If `true` and the hook has a `tokenUriResolver`, resolves the URI and includes it.
|
|
205
|
+
/// @return The tier that the NFT belongs to.
|
|
200
206
|
function tierOfTokenId(
|
|
201
207
|
address hook,
|
|
202
208
|
uint256 tokenId,
|
|
@@ -255,14 +261,14 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
255
261
|
transfersPausable = (storedTier.packedBools & 0x2) != 0;
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
/// @notice
|
|
259
|
-
///
|
|
260
|
-
/// @param
|
|
261
|
-
/// @param
|
|
262
|
-
///
|
|
263
|
-
/// @param startingId
|
|
264
|
-
/// @param size The number of tiers to
|
|
265
|
-
/// @return tiers An array of active
|
|
264
|
+
/// @notice Get all active (non-removed) tiers for a hook, with optional filtering by category and pagination.
|
|
265
|
+
/// Tiers are returned sorted by category.
|
|
266
|
+
/// @param hook The 721 hook contract to get tiers from.
|
|
267
|
+
/// @param categories Filter to specific categories. Pass an empty array to include all categories.
|
|
268
|
+
/// @param includeResolvedUri If `true` and the hook has a `tokenUriResolver`, resolves URIs and includes them.
|
|
269
|
+
/// @param startingId Start from this tier ID (for pagination). Pass 0 to start from the beginning.
|
|
270
|
+
/// @param size The maximum number of tiers to return.
|
|
271
|
+
/// @return tiers An array of active tiers.
|
|
266
272
|
function tiersOf(
|
|
267
273
|
address hook,
|
|
268
274
|
uint256[] calldata categories,
|
|
@@ -345,14 +351,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
345
351
|
}
|
|
346
352
|
}
|
|
347
353
|
|
|
348
|
-
/// @notice
|
|
349
|
-
///
|
|
350
|
-
/// @
|
|
351
|
-
///
|
|
352
|
-
/// @param
|
|
353
|
-
/// @
|
|
354
|
-
/// @param tierId The ID of the tier to get voting units within.
|
|
355
|
-
/// @return The address' voting units within the tier.
|
|
354
|
+
/// @notice Get an address's voting power from a specific tier. Each NFT in the tier contributes either the tier's
|
|
355
|
+
/// custom `votingUnits` (if configured) or the tier's price. Multiply by the holder's balance in the tier.
|
|
356
|
+
/// @param hook The 721 hook contract that the tier belongs to.
|
|
357
|
+
/// @param account The address to get voting units for.
|
|
358
|
+
/// @param tierId The ID of the tier.
|
|
359
|
+
/// @return The address's total voting units within the tier.
|
|
356
360
|
function tierVotingUnitsOf(
|
|
357
361
|
address hook,
|
|
358
362
|
address account,
|
|
@@ -379,9 +383,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
379
383
|
return balance * (useVotingUnits ? _tierVotingUnitsOf[hook][tierId] : storedTier.price);
|
|
380
384
|
}
|
|
381
385
|
|
|
382
|
-
/// @notice
|
|
383
|
-
/// @param hook The 721 contract to get
|
|
384
|
-
/// @return supply The total number of NFTs
|
|
386
|
+
/// @notice The total number of NFTs currently in circulation for a hook (minted minus burned, across all tiers).
|
|
387
|
+
/// @param hook The 721 hook contract to get the total supply of.
|
|
388
|
+
/// @return supply The total number of outstanding NFTs.
|
|
385
389
|
function totalSupplyOf(address hook) external view override returns (uint256 supply) {
|
|
386
390
|
// Keep a reference to the greatest tier ID.
|
|
387
391
|
uint256 maxTierId = maxTierIdOf[hook];
|
|
@@ -402,13 +406,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
402
406
|
}
|
|
403
407
|
}
|
|
404
408
|
|
|
405
|
-
/// @notice Get
|
|
406
|
-
///
|
|
407
|
-
/// @dev
|
|
408
|
-
///
|
|
409
|
-
/// @param hook The 721 contract to get the voting units within.
|
|
409
|
+
/// @notice Get an address's total voting power across all tiers of a hook. Sums up the voting units from every
|
|
410
|
+
/// tier where the address holds NFTs.
|
|
411
|
+
/// @dev Each tier contributes: `balance * (customVotingUnits || tierPrice)`.
|
|
412
|
+
/// @param hook The 721 hook contract to get voting units within.
|
|
410
413
|
/// @param account The address to get the voting unit total of.
|
|
411
|
-
/// @return units The total voting units the address
|
|
414
|
+
/// @return units The total voting units the address holds across all tiers.
|
|
412
415
|
function votingUnitsOf(address hook, address account) external view virtual override returns (uint256 units) {
|
|
413
416
|
// Keep a reference to the greatest tier ID.
|
|
414
417
|
uint256 maxTierId = maxTierIdOf[hook];
|
|
@@ -446,11 +449,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
446
449
|
// -------------------------- public views --------------------------- //
|
|
447
450
|
//*********************************************************************//
|
|
448
451
|
|
|
449
|
-
/// @notice
|
|
450
|
-
///
|
|
451
|
-
/// @param hook The 721 contract to get the balance within.
|
|
452
|
+
/// @notice How many NFTs an address owns from a hook, totaled across all tiers.
|
|
453
|
+
/// @param hook The 721 hook contract to check.
|
|
452
454
|
/// @param owner The address to check the balance of.
|
|
453
|
-
/// @return balance The number of NFTs the owner
|
|
455
|
+
/// @return balance The total number of NFTs the owner holds.
|
|
454
456
|
function balanceOf(address hook, address owner) public view override returns (uint256 balance) {
|
|
455
457
|
// Keep a reference to the greatest tier ID.
|
|
456
458
|
uint256 maxTierId = maxTierIdOf[hook];
|
|
@@ -466,13 +468,13 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
466
468
|
}
|
|
467
469
|
}
|
|
468
470
|
|
|
469
|
-
/// @notice The combined cash
|
|
470
|
-
///
|
|
471
|
-
/// @dev
|
|
472
|
-
///
|
|
473
|
-
/// @param hook The 721 contract
|
|
474
|
-
/// @param tokenIds The token IDs
|
|
475
|
-
/// @return weight The cash
|
|
471
|
+
/// @notice The combined cash-out weight of specific NFTs. Divide by `totalCashOutWeight` to get the fraction of
|
|
472
|
+
/// the project's surplus that cashing out these NFTs would reclaim.
|
|
473
|
+
/// @dev Weight is based on each NFT's original tier price (not the discounted price paid). Discounts are
|
|
474
|
+
/// transient purchase incentives and don't affect an NFT's share of the cash-out pool.
|
|
475
|
+
/// @param hook The 721 hook contract the NFTs belong to.
|
|
476
|
+
/// @param tokenIds The token IDs to get the combined cash-out weight of.
|
|
477
|
+
/// @return weight The combined cash-out weight.
|
|
476
478
|
function cashOutWeightOf(address hook, uint256[] calldata tokenIds) public view override returns (uint256 weight) {
|
|
477
479
|
// Add each 721's original price (from its tier) to the weight.
|
|
478
480
|
// Uses the full tier price, not the discounted price — by design. Discounts are transient incentives
|
|
@@ -488,10 +490,11 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
488
490
|
}
|
|
489
491
|
}
|
|
490
492
|
|
|
491
|
-
/// @notice The
|
|
492
|
-
///
|
|
493
|
-
/// @param
|
|
494
|
-
/// @
|
|
493
|
+
/// @notice The address that receives reserved NFTs for a tier. Falls back to the hook's default reserve
|
|
494
|
+
/// beneficiary if no tier-specific beneficiary is set.
|
|
495
|
+
/// @param hook The 721 hook contract.
|
|
496
|
+
/// @param tierId The ID of the tier.
|
|
497
|
+
/// @return The reserve beneficiary address for the tier.
|
|
495
498
|
function reserveBeneficiaryOf(address hook, uint256 tierId) public view override returns (address) {
|
|
496
499
|
// Get the stored reserve beneficiary.
|
|
497
500
|
address storedReserveBeneficiaryOfTier = _reserveBeneficiaryOf[hook][tierId];
|
|
@@ -505,19 +508,18 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
505
508
|
return defaultReserveBeneficiaryOf[hook];
|
|
506
509
|
}
|
|
507
510
|
|
|
508
|
-
/// @notice
|
|
509
|
-
///
|
|
510
|
-
/// @param tokenId The token ID of the
|
|
511
|
-
/// @return The ID
|
|
511
|
+
/// @notice Derive which tier an NFT belongs to from its token ID. Token IDs encode the tier: `tokenId / 1e9`
|
|
512
|
+
/// gives the tier ID.
|
|
513
|
+
/// @param tokenId The token ID of the NFT.
|
|
514
|
+
/// @return The tier ID.
|
|
512
515
|
function tierIdOfToken(uint256 tokenId) public pure override returns (uint256) {
|
|
513
516
|
return tokenId / _ONE_BILLION;
|
|
514
517
|
}
|
|
515
518
|
|
|
516
|
-
/// @notice Get the tier
|
|
517
|
-
/// @param hook The 721 contract to get the tier from.
|
|
519
|
+
/// @notice Get the full details of a specific tier by its ID — price, supply, reserve info, metadata, etc.
|
|
520
|
+
/// @param hook The 721 hook contract to get the tier from.
|
|
518
521
|
/// @param id The ID of the tier to get.
|
|
519
|
-
/// @param includeResolvedUri If
|
|
520
|
-
/// resolved and included.
|
|
522
|
+
/// @param includeResolvedUri If `true` and the hook has a `tokenUriResolver`, resolves the URI and includes it.
|
|
521
523
|
/// @return The tier.
|
|
522
524
|
function tierOf(address hook, uint256 id, bool includeResolvedUri) public view override returns (JB721Tier memory) {
|
|
523
525
|
return _getTierFrom({
|
|
@@ -525,9 +527,10 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
525
527
|
});
|
|
526
528
|
}
|
|
527
529
|
|
|
528
|
-
/// @notice The
|
|
529
|
-
///
|
|
530
|
-
/// @
|
|
530
|
+
/// @notice The total cash-out weight across all outstanding NFTs (including pending reserves). This is the
|
|
531
|
+
/// denominator used to determine what fraction of a project's surplus each NFT can reclaim on cash out.
|
|
532
|
+
/// @param hook The 721 hook contract to get the total cash-out weight of.
|
|
533
|
+
/// @return weight The total cash-out weight.
|
|
531
534
|
// Changing defaultReserveBeneficiary retroactively affects totalCashOutWeight. By design —
|
|
532
535
|
// cashOutWeight is calculated dynamically, not snapshotted. The defaultReserveBeneficiary determines which
|
|
533
536
|
// tiers have pending reserves (via _numberOfPendingReservesFor), affecting the denominator. Changing it is
|
|
@@ -615,7 +618,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
615
618
|
// Cache packed bools to avoid stack-too-deep from destructuring all 6 bools.
|
|
616
619
|
uint8 packed = storedTier.packedBools;
|
|
617
620
|
|
|
618
|
-
// slither-disable-next-line calls-loop
|
|
619
621
|
return JB721Tier({
|
|
620
622
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
621
623
|
id: uint32(tierId),
|
|
@@ -752,13 +754,15 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
752
754
|
}
|
|
753
755
|
}
|
|
754
756
|
|
|
755
|
-
/// @notice Pack
|
|
756
|
-
/// @
|
|
757
|
-
///
|
|
758
|
-
/// @param
|
|
759
|
-
/// @param
|
|
760
|
-
/// @param
|
|
761
|
-
/// @param
|
|
757
|
+
/// @notice Pack six tier-level boolean flags into a single uint8 for compact storage in `JBStored721Tier`.
|
|
758
|
+
/// @dev Bit layout: 0=allowOwnerMint, 1=transfersPausable, 2=useVotingUnits, 3=cantBeRemoved,
|
|
759
|
+
/// 4=cantIncreaseDiscountPercent, 5=cantBuyWithCredits.
|
|
760
|
+
/// @param allowOwnerMint Whether the project owner can mint from this tier directly (without paying).
|
|
761
|
+
/// @param transfersPausable Whether transfers of NFTs from this tier can be paused by the ruleset.
|
|
762
|
+
/// @param useVotingUnits Whether this tier uses a custom voting power value instead of defaulting to its price.
|
|
763
|
+
/// @param cantBeRemoved Whether this tier is permanently locked and cannot be removed once added.
|
|
764
|
+
/// @param cantIncreaseDiscountPercent Whether the discount percent can only stay the same or decrease.
|
|
765
|
+
/// @param cantBuyWithCredits Whether this tier cannot be purchased using accumulated pay credits.
|
|
762
766
|
/// @return packed The packed bools.
|
|
763
767
|
function _packBools(
|
|
764
768
|
bool allowOwnerMint,
|
|
@@ -782,14 +786,16 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
782
786
|
}
|
|
783
787
|
}
|
|
784
788
|
|
|
785
|
-
/// @notice Unpack six
|
|
789
|
+
/// @notice Unpack six tier-level boolean flags from a single uint8 stored in `JBStored721Tier.packedBools`.
|
|
790
|
+
/// @dev Inverse of `_packBools`. Same bit layout: 0=allowOwnerMint, 1=transfersPausable, 2=useVotingUnits,
|
|
791
|
+
/// 3=cantBeRemoved, 4=cantIncreaseDiscountPercent, 5=cantBuyWithCredits.
|
|
786
792
|
/// @param packed The packed bools.
|
|
787
|
-
/// @param allowOwnerMint Whether
|
|
788
|
-
/// @param transfersPausable Whether
|
|
789
|
-
/// @param useVotingUnits Whether
|
|
790
|
-
/// @param cantBeRemoved Whether
|
|
791
|
-
/// @param cantIncreaseDiscountPercent Whether
|
|
792
|
-
/// @param cantBuyWithCredits Whether
|
|
793
|
+
/// @param allowOwnerMint Whether the project owner can mint from this tier directly (without paying).
|
|
794
|
+
/// @param transfersPausable Whether transfers of NFTs from this tier can be paused by the ruleset.
|
|
795
|
+
/// @param useVotingUnits Whether this tier uses a custom voting power value instead of defaulting to its price.
|
|
796
|
+
/// @param cantBeRemoved Whether this tier is permanently locked and cannot be removed once added.
|
|
797
|
+
/// @param cantIncreaseDiscountPercent Whether the discount percent can only stay the same or decrease.
|
|
798
|
+
/// @param cantBuyWithCredits Whether this tier cannot be purchased using accumulated pay credits.
|
|
793
799
|
function _unpackBools(uint8 packed)
|
|
794
800
|
internal
|
|
795
801
|
pure
|
|
@@ -816,7 +822,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
816
822
|
// ---------------------- external transactions ---------------------- //
|
|
817
823
|
//*********************************************************************//
|
|
818
824
|
|
|
819
|
-
/// @notice
|
|
825
|
+
/// @notice Walk through the tier sorting sequence and skip over any tiers that have been removed. This compacts
|
|
826
|
+
/// the linked list so that `tiersOf` iteration no longer visits removed tiers. Anyone can call this — it's a
|
|
827
|
+
/// maintenance operation with no access control.
|
|
828
|
+
/// @dev Call this after `recordRemoveTierIds` to keep tier iteration efficient. Without cleaning, removed tiers
|
|
829
|
+
/// remain in the linked list (they just can't be minted from). The function also updates
|
|
830
|
+
/// `_startingTierIdOfCategory` if the previous starting tier for a category was removed.
|
|
820
831
|
/// @param hook The 721 contract to clean tiers for.
|
|
821
832
|
function cleanTiers(address hook) external override {
|
|
822
833
|
// Keep a reference to the last tier ID.
|
|
@@ -826,11 +837,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
826
837
|
uint256 currentSortedTierId = _firstSortedTierIdOf({hook: hook, category: 0});
|
|
827
838
|
|
|
828
839
|
// Keep track of the previous non-removed tier ID.
|
|
829
|
-
// slither-disable-next-line uninitialized-local
|
|
830
840
|
uint256 previousSortedTierId;
|
|
831
841
|
|
|
832
842
|
// Initialize a `JBBitmapWord` for tracking removed tiers.
|
|
833
|
-
// slither-disable-next-line uninitialized-local
|
|
834
843
|
JBBitmapWord memory bitmapWord;
|
|
835
844
|
|
|
836
845
|
// Make the sorted array.
|
|
@@ -873,13 +882,16 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
873
882
|
emit CleanTiers({hook: hook, caller: msg.sender});
|
|
874
883
|
}
|
|
875
884
|
|
|
876
|
-
/// @notice
|
|
877
|
-
///
|
|
885
|
+
/// @notice Validate and store new tiers for the calling hook. Each tier is assigned a sequential ID, inserted
|
|
886
|
+
/// into the category-sorted linked list, and has its reserve beneficiary, voting units, IPFS URI, and flags
|
|
887
|
+
/// persisted. Tiers must be provided sorted by category (ascending).
|
|
888
|
+
/// @dev Only callable by hook contracts (msg.sender is treated as the hook address).
|
|
889
|
+
/// WARNING: If any tier in `tiersToAdd` has `useReserveBeneficiaryAsDefault` set to `true`, its
|
|
878
890
|
/// `reserveBeneficiary` will overwrite the hook's global `defaultReserveBeneficiaryOf`. This affects ALL existing
|
|
879
891
|
/// tiers that do not have a tier-specific reserve beneficiary set via `_reserveBeneficiaryOf`. Callers should be
|
|
880
892
|
/// aware of this side effect when using `adjustTiers` to add new tiers.
|
|
881
893
|
/// @param tiersToAdd The tiers to add.
|
|
882
|
-
/// @return tierIds The IDs of the tiers
|
|
894
|
+
/// @return tierIds The IDs of the tiers added.
|
|
883
895
|
function recordAddTiers(JB721TierConfig[] calldata tiersToAdd)
|
|
884
896
|
external
|
|
885
897
|
override
|
|
@@ -890,7 +902,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
890
902
|
|
|
891
903
|
// Make sure the max number of tiers won't be exceeded.
|
|
892
904
|
if (currentMaxTierIdOf + tiersToAdd.length > type(uint16).max) {
|
|
893
|
-
revert JB721TiersHookStore_MaxTiersExceeded(
|
|
905
|
+
revert JB721TiersHookStore_MaxTiersExceeded({
|
|
906
|
+
numberOfTiers: currentMaxTierIdOf + tiersToAdd.length, limit: type(uint16).max
|
|
907
|
+
});
|
|
894
908
|
}
|
|
895
909
|
|
|
896
910
|
// Keep a reference to the current last sorted tier ID (sorted by category).
|
|
@@ -904,7 +918,6 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
904
918
|
uint256 startSortedTierId = currentMaxTierIdOf == 0 ? 0 : _firstSortedTierIdOf({hook: msg.sender, category: 0});
|
|
905
919
|
|
|
906
920
|
// Keep track of the previous tier's ID while iterating.
|
|
907
|
-
// slither-disable-next-line uninitialized-local
|
|
908
921
|
uint256 previousTierId;
|
|
909
922
|
|
|
910
923
|
// Keep a reference to the 721 contract's flags.
|
|
@@ -917,7 +930,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
917
930
|
// Make sure the supply maximum is enforced. If it's greater than one billion, it would overflow into the
|
|
918
931
|
// next tier.
|
|
919
932
|
if (tierToAdd.initialSupply > _ONE_BILLION - 1) {
|
|
920
|
-
revert JB721TiersHookStore_InvalidQuantity(tierToAdd.initialSupply, _ONE_BILLION - 1);
|
|
933
|
+
revert JB721TiersHookStore_InvalidQuantity({quantity: tierToAdd.initialSupply, limit: _ONE_BILLION - 1});
|
|
921
934
|
}
|
|
922
935
|
|
|
923
936
|
// Make sure the tier's category is greater than or equal to the previously added tier's category.
|
|
@@ -927,7 +940,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
927
940
|
|
|
928
941
|
// Revert if the category is not equal or greater than the previously added tier's category.
|
|
929
942
|
if (tierToAdd.category < previousCategory) {
|
|
930
|
-
revert JB721TiersHookStore_InvalidCategorySortOrder(
|
|
943
|
+
revert JB721TiersHookStore_InvalidCategorySortOrder({
|
|
944
|
+
tierCategory: tierToAdd.category, previousTierCategory: previousCategory
|
|
945
|
+
});
|
|
931
946
|
}
|
|
932
947
|
}
|
|
933
948
|
|
|
@@ -940,32 +955,32 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
940
955
|
&& ((tierToAdd.flags.useVotingUnits && tierToAdd.votingUnits != 0)
|
|
941
956
|
|| (!tierToAdd.flags.useVotingUnits && tierToAdd.price != 0))
|
|
942
957
|
) {
|
|
943
|
-
revert JB721TiersHookStore_VotingUnitsNotAllowed(tierId);
|
|
958
|
+
revert JB721TiersHookStore_VotingUnitsNotAllowed({tierId: tierId});
|
|
944
959
|
}
|
|
945
960
|
|
|
946
961
|
// Make sure the new tier doesn't have a reserve frequency if the 721 contract's flags don't allow it to,
|
|
947
962
|
// OR if manual minting is allowed.
|
|
948
963
|
if ((flags.noNewTiersWithReserves || tierToAdd.flags.allowOwnerMint) && tierToAdd.reserveFrequency != 0) {
|
|
949
|
-
revert JB721TiersHookStore_ReserveFrequencyNotAllowed(tierId);
|
|
964
|
+
revert JB721TiersHookStore_ReserveFrequencyNotAllowed({tierId: tierId});
|
|
950
965
|
}
|
|
951
966
|
|
|
952
967
|
// Make sure the new tier doesn't have owner minting enabled if the 721 contract's flags don't allow it to.
|
|
953
968
|
if (flags.noNewTiersWithOwnerMinting && tierToAdd.flags.allowOwnerMint) {
|
|
954
|
-
revert JB721TiersHookStore_ManualMintingNotAllowed(tierId);
|
|
969
|
+
revert JB721TiersHookStore_ManualMintingNotAllowed({tierId: tierId});
|
|
955
970
|
}
|
|
956
971
|
|
|
957
972
|
// Make sure the discount percent is within the bound.
|
|
958
973
|
if (tierToAdd.discountPercent > JB721Constants.DISCOUNT_DENOMINATOR) {
|
|
959
|
-
revert JB721TiersHookStore_DiscountPercentExceedsBounds(
|
|
960
|
-
tierToAdd.discountPercent, JB721Constants.DISCOUNT_DENOMINATOR
|
|
961
|
-
);
|
|
974
|
+
revert JB721TiersHookStore_DiscountPercentExceedsBounds({
|
|
975
|
+
percent: tierToAdd.discountPercent, limit: JB721Constants.DISCOUNT_DENOMINATOR
|
|
976
|
+
});
|
|
962
977
|
}
|
|
963
978
|
|
|
964
979
|
// Make sure the split percent is within the bound.
|
|
965
980
|
if (tierToAdd.splitPercent > JBConstants.SPLITS_TOTAL_PERCENT) {
|
|
966
|
-
revert JB721TiersHookStore_SplitPercentExceedsBounds(
|
|
967
|
-
tierToAdd.splitPercent, JBConstants.SPLITS_TOTAL_PERCENT
|
|
968
|
-
);
|
|
981
|
+
revert JB721TiersHookStore_SplitPercentExceedsBounds({
|
|
982
|
+
percent: tierToAdd.splitPercent, limit: JBConstants.SPLITS_TOTAL_PERCENT
|
|
983
|
+
});
|
|
969
984
|
}
|
|
970
985
|
|
|
971
986
|
// Make sure the tier has a non-zero supply.
|
|
@@ -974,7 +989,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
974
989
|
// A tier with initialSupply == 1 and reserveFrequency > 0 deadlocks: the single mint is reserved,
|
|
975
990
|
// leaving zero available for paid mints, but reserves only mint after a paid mint triggers them.
|
|
976
991
|
if (tierToAdd.initialSupply == 1 && tierToAdd.reserveFrequency > 0) {
|
|
977
|
-
revert JB721TiersHookStore_DeadlockedReserve(
|
|
992
|
+
revert JB721TiersHookStore_DeadlockedReserve({
|
|
993
|
+
tierId: tierId, initialSupply: tierToAdd.initialSupply, reserveFrequency: tierToAdd.reserveFrequency
|
|
994
|
+
});
|
|
978
995
|
}
|
|
979
996
|
|
|
980
997
|
// A tier with reserves must have a beneficiary — either tier-specific or a previously set default.
|
|
@@ -983,7 +1000,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
983
1000
|
tierToAdd.reserveFrequency > 0 && tierToAdd.reserveBeneficiary == address(0)
|
|
984
1001
|
&& defaultReserveBeneficiaryOf[msg.sender] == address(0)
|
|
985
1002
|
) {
|
|
986
|
-
revert JB721TiersHookStore_MissingReserveBeneficiary(tierId);
|
|
1003
|
+
revert JB721TiersHookStore_MissingReserveBeneficiary({tierId: tierId});
|
|
987
1004
|
}
|
|
988
1005
|
|
|
989
1006
|
// Store the tier with that ID.
|
|
@@ -1125,7 +1142,8 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1125
1142
|
maxTierIdOf[msg.sender] = currentMaxTierIdOf + tiersToAdd.length;
|
|
1126
1143
|
}
|
|
1127
1144
|
|
|
1128
|
-
/// @notice
|
|
1145
|
+
/// @notice Increment the burn counter for each token's tier. Does NOT affect `remainingSupply` — burned NFTs
|
|
1146
|
+
/// cannot be re-minted. The burn count is used by `totalSupplyOf` to compute the circulating supply.
|
|
1129
1147
|
/// @dev This function trusts `msg.sender` (the hook contract) to only call it after actually burning the
|
|
1130
1148
|
/// tokens. It does not verify ownership or existence of the token IDs — the hook is responsible for
|
|
1131
1149
|
/// performing those checks before calling this function.
|
|
@@ -1147,16 +1165,22 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1147
1165
|
}
|
|
1148
1166
|
}
|
|
1149
1167
|
|
|
1150
|
-
/// @notice
|
|
1168
|
+
/// @notice Store the behavioral flags for the calling hook. These flags govern whether new tiers can have voting
|
|
1169
|
+
/// units, reserves, or owner minting enabled.
|
|
1170
|
+
/// @dev Only callable by hook contracts. Overwrites any previously stored flags for the caller.
|
|
1151
1171
|
/// @param flags The flags to set.
|
|
1152
1172
|
function recordFlags(JB721TiersHookFlags calldata flags) external override {
|
|
1153
1173
|
_flagsOf[msg.sender] = flags;
|
|
1154
1174
|
}
|
|
1155
1175
|
|
|
1156
|
-
/// @notice Record
|
|
1157
|
-
///
|
|
1176
|
+
/// @notice Record paid mints: deduct each tier's (discounted) price from `amount`, decrement supply, generate
|
|
1177
|
+
/// token IDs, and enforce that enough supply remains to satisfy pending reserves. Returns the leftover amount
|
|
1178
|
+
/// and the total cost of credit-restricted tiers so the hook can enforce pay-credit rules.
|
|
1179
|
+
/// @dev Reverts if the tier is removed, unrecognized, sold out, or its price exceeds the remaining amount.
|
|
1180
|
+
/// For owner mints, the tier must have `allowOwnerMint` set.
|
|
1181
|
+
/// @param amount The amount to spend on NFTs. The total price must not exceed this amount.
|
|
1158
1182
|
/// @param tierIds The IDs of the tiers to mint from.
|
|
1159
|
-
/// @param isOwnerMint A flag indicating whether
|
|
1183
|
+
/// @param isOwnerMint A flag indicating whether the 721 contract's owner is directly calling this function.
|
|
1160
1184
|
/// @return tokenIds The token IDs of the NFTs which were minted.
|
|
1161
1185
|
/// @return leftoverAmount The `amount` remaining after minting.
|
|
1162
1186
|
/// @return restrictedCost Total cost of tiers with `cantBuyWithCredits` set. The caller can use this to enforce
|
|
@@ -1191,7 +1215,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1191
1215
|
|
|
1192
1216
|
// Make sure the tier hasn't been removed.
|
|
1193
1217
|
if (_isTierRemovedWithRefresh({hook: msg.sender, tierId: tierId, bitmapWord: bitmapWord})) {
|
|
1194
|
-
revert JB721TiersHookStore_TierRemoved(tierId);
|
|
1218
|
+
revert JB721TiersHookStore_TierRemoved({tierId: tierId});
|
|
1195
1219
|
}
|
|
1196
1220
|
|
|
1197
1221
|
// Keep a reference to the stored tier being iterated on.
|
|
@@ -1243,7 +1267,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1243
1267
|
storedTier.remainingSupply
|
|
1244
1268
|
< _numberOfPendingReservesFor({hook: msg.sender, tierId: tierId, storedTier: storedTier})
|
|
1245
1269
|
) {
|
|
1246
|
-
revert JB721TiersHookStore_InsufficientSupplyRemaining(tierId);
|
|
1270
|
+
revert JB721TiersHookStore_InsufficientSupplyRemaining({tierId: tierId});
|
|
1247
1271
|
}
|
|
1248
1272
|
|
|
1249
1273
|
unchecked {
|
|
@@ -1252,7 +1276,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1252
1276
|
}
|
|
1253
1277
|
}
|
|
1254
1278
|
|
|
1255
|
-
/// @notice
|
|
1279
|
+
/// @notice Generate token IDs for reserve NFT mints and decrement the tier's remaining supply. Reserves
|
|
1280
|
+
/// accumulate as non-reserve NFTs are minted (one reserve per `reserveFrequency` mints). Reverts if `count`
|
|
1281
|
+
/// exceeds the number of pending reserves.
|
|
1256
1282
|
/// @param tierId The ID of the tier to mint reserves from.
|
|
1257
1283
|
/// @param count The number of reserve NFTs to mint.
|
|
1258
1284
|
/// @return tokenIds The token IDs of the reserve NFTs which were minted.
|
|
@@ -1271,7 +1297,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1271
1297
|
|
|
1272
1298
|
// Can't mint more than the number of pending reserves.
|
|
1273
1299
|
if (count > numberOfPendingReserves) {
|
|
1274
|
-
revert JB721TiersHookStore_InsufficientPendingReserves(
|
|
1300
|
+
revert JB721TiersHookStore_InsufficientPendingReserves({
|
|
1301
|
+
count: count, numberOfPendingReserves: numberOfPendingReserves
|
|
1302
|
+
});
|
|
1275
1303
|
}
|
|
1276
1304
|
|
|
1277
1305
|
// Increment the number of reserve NFTs minted.
|
|
@@ -1292,10 +1320,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1292
1320
|
}
|
|
1293
1321
|
}
|
|
1294
1322
|
|
|
1295
|
-
/// @notice
|
|
1323
|
+
/// @notice Mark one or more tiers as removed. Removed tiers cannot be minted from, but existing NFTs from those
|
|
1324
|
+
/// tiers remain valid (can still be cashed out and transferred). Pending reserves can still be minted.
|
|
1296
1325
|
/// @dev Removing a tier only marks it in a bitmap — it does not update the sorted tier linked list.
|
|
1297
1326
|
/// Call `cleanTiers()` after removing tiers to update the sorting sequence and prevent stale tier iteration.
|
|
1298
|
-
///
|
|
1327
|
+
/// Reverts if the tier has `cantBeRemoved` set, or if the tier ID is 0 or exceeds `maxTierIdOf`.
|
|
1328
|
+
/// @param tierIds The IDs of the tiers to remove.
|
|
1299
1329
|
function recordRemoveTierIds(uint256[] calldata tierIds) external override {
|
|
1300
1330
|
for (uint256 i; i < tierIds.length;) {
|
|
1301
1331
|
// Set the tier being iterated upon (0-indexed).
|
|
@@ -1304,7 +1334,7 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1304
1334
|
// Reject tier IDs that don't exist yet — removing a future tier would cause it
|
|
1305
1335
|
// to be born already removed when later added.
|
|
1306
1336
|
if (tierId == 0 || tierId > maxTierIdOf[msg.sender]) {
|
|
1307
|
-
revert JB721TiersHookStore_UnrecognizedTier(tierId);
|
|
1337
|
+
revert JB721TiersHookStore_UnrecognizedTier({tierId: tierId});
|
|
1308
1338
|
}
|
|
1309
1339
|
|
|
1310
1340
|
// Get a reference to the stored tier.
|
|
@@ -1325,9 +1355,11 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1325
1355
|
}
|
|
1326
1356
|
}
|
|
1327
1357
|
|
|
1328
|
-
/// @notice
|
|
1358
|
+
/// @notice Update the discount percentage for a tier. Discounts reduce the price payers pay without affecting the
|
|
1359
|
+
/// NFT's cash-out weight (which always uses the original price). Reverts if the tier is removed, if the percent
|
|
1360
|
+
/// exceeds the denominator, or if the tier has `cantIncreaseDiscountPercent` set and the new value is higher.
|
|
1329
1361
|
/// @param tierId The ID of the tier to record a discount for.
|
|
1330
|
-
/// @param discountPercent The new discount percent
|
|
1362
|
+
/// @param discountPercent The new discount percent to apply.
|
|
1331
1363
|
function recordSetDiscountPercentOf(uint256 tierId, uint256 discountPercent) external override {
|
|
1332
1364
|
// Make sure the tier hasn't been removed.
|
|
1333
1365
|
JBBitmapWord memory bitmapWord = _removedTiersBitmapWordOf[msg.sender].readId(tierId);
|
|
@@ -1335,9 +1367,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1335
1367
|
|
|
1336
1368
|
// Make sure the discount percent is within the bound.
|
|
1337
1369
|
if (discountPercent > JB721Constants.DISCOUNT_DENOMINATOR) {
|
|
1338
|
-
revert JB721TiersHookStore_DiscountPercentExceedsBounds(
|
|
1339
|
-
discountPercent, JB721Constants.DISCOUNT_DENOMINATOR
|
|
1340
|
-
);
|
|
1370
|
+
revert JB721TiersHookStore_DiscountPercentExceedsBounds({
|
|
1371
|
+
percent: discountPercent, limit: JB721Constants.DISCOUNT_DENOMINATOR
|
|
1372
|
+
});
|
|
1341
1373
|
}
|
|
1342
1374
|
|
|
1343
1375
|
// Get a reference to the stored tier.
|
|
@@ -1348,7 +1380,9 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1348
1380
|
|
|
1349
1381
|
// Make sure that increasing the discount is allowed for the tier.
|
|
1350
1382
|
if (discountPercent > storedTier.discountPercent && cantIncreaseDiscountPercent) {
|
|
1351
|
-
revert JB721TiersHookStore_DiscountPercentIncreaseNotAllowed(
|
|
1383
|
+
revert JB721TiersHookStore_DiscountPercentIncreaseNotAllowed({
|
|
1384
|
+
percent: discountPercent, storedPercent: storedTier.discountPercent
|
|
1385
|
+
});
|
|
1352
1386
|
}
|
|
1353
1387
|
|
|
1354
1388
|
// Set the discount.
|
|
@@ -1370,10 +1404,12 @@ contract JB721TiersHookStore is IJB721TiersHookStore {
|
|
|
1370
1404
|
tokenUriResolverOf[msg.sender] = resolver;
|
|
1371
1405
|
}
|
|
1372
1406
|
|
|
1373
|
-
/// @notice
|
|
1374
|
-
///
|
|
1375
|
-
///
|
|
1376
|
-
/// @param
|
|
1407
|
+
/// @notice Update tier balance accounting when an NFT is transferred. Decrements the sender's balance and
|
|
1408
|
+
/// increments the receiver's balance for the given tier. Handles mints (from == address(0)) and burns
|
|
1409
|
+
/// (to == address(0)) as one-sided updates.
|
|
1410
|
+
/// @param tierId The ID of the tier that the 721 to transfer belongs to.
|
|
1411
|
+
/// @param from The address to transfer the 721 from.
|
|
1412
|
+
/// @param to The address to transfer the 721 to.
|
|
1377
1413
|
function recordTransferForTier(uint256 tierId, address from, address to) external override {
|
|
1378
1414
|
// If this is not a mint,
|
|
1379
1415
|
if (from != address(0)) {
|